Ashok Kumar Bhati commited on
Commit
cc7f27e
·
1 Parent(s): 08af573

Add user register API endpoint

Browse files
migrations/versions/8652e8501339_add_user_and_slack_team.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """add user and slack team
2
+
3
+ Revision ID: 8652e8501339
4
+ Revises:
5
+ Create Date: 2024-10-24 16:52:59.936776
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '8652e8501339'
16
+ down_revision: Union[str, None] = None
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ op.create_table('slack_teams',
24
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
25
+ sa.Column('name', sa.String(), nullable=False),
26
+ sa.Column('slack_team_id', sa.String(), nullable=False),
27
+ sa.Column('token', sa.String(), nullable=False),
28
+ sa.Column('created_at', sa.DateTime(), nullable=False),
29
+ sa.PrimaryKeyConstraint('id')
30
+ )
31
+ op.create_table('users',
32
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
33
+ sa.Column('name', sa.String(), nullable=False),
34
+ sa.Column('content_delivery_frequency', sa.Enum('DAILY', 'WEEKLY', 'BI_WEEKLY', 'MONTHLY', name='contentdeliveryfrequency'), nullable=False),
35
+ sa.Column('slack_id', sa.String(), nullable=True),
36
+ sa.Column('slack_team_id', sa.Integer(), nullable=True),
37
+ sa.Column('email_id', sa.String(), nullable=False),
38
+ sa.Column('last_message_sent_at', sa.DateTime(), nullable=True),
39
+ sa.Column('created_at', sa.DateTime(), nullable=False),
40
+ sa.ForeignKeyConstraint(['slack_team_id'], ['slack_teams.id'], ),
41
+ sa.PrimaryKeyConstraint('id')
42
+ )
43
+ # ### end Alembic commands ###
44
+
45
+
46
+ def downgrade() -> None:
47
+ # ### commands auto generated by Alembic - please adjust! ###
48
+ op.drop_table('users')
49
+ op.drop_table('slack_teams')
50
+ # ### end Alembic commands ###
src/app.py CHANGED
@@ -9,7 +9,11 @@ from fastapi.middleware.cors import CORSMiddleware
9
 
10
  from src.services import ContentDeliveryService
11
  from src.utils import logger
12
- from src.controllers import bot_router
 
 
 
 
13
 
14
  scheduler = AsyncIOScheduler()
15
 
@@ -23,6 +27,21 @@ async def scheduled_job():
23
  logger.error(e)
24
 
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  def run_scheduler():
27
  loop = asyncio.new_event_loop()
28
  asyncio.set_event_loop(loop)
@@ -34,6 +53,7 @@ def run_scheduler():
34
  async def lifespan(app: FastAPI):
35
  try:
36
  logger.info("Starting up the application...")
 
37
  scheduler_thread = Thread(target=run_scheduler, daemon=True)
38
  scheduler_thread.start()
39
  logger.info("Application started successfully...")
@@ -63,3 +83,4 @@ async def check_health():
63
  return {"response": "Service is healthy!"}
64
 
65
  app.include_router(bot_router, prefix="/api/v1")
 
 
9
 
10
  from src.services import ContentDeliveryService
11
  from src.utils import logger
12
+ from src.controllers import bot_router, user_router
13
+
14
+ from alembic import command
15
+ from alembic.config import Config
16
+ from src.repositories import DatabaseConfig
17
 
18
  scheduler = AsyncIOScheduler()
19
 
 
27
  logger.error(e)
28
 
29
 
30
+ def run_upgrade(connection, alembic_config: Config):
31
+ alembic_config.attributes["connection"] = connection
32
+ command.upgrade(alembic_config, "head")
33
+
34
+
35
+ async def run_migrations():
36
+ logger.info("Running migrations if any...")
37
+ alembic_config = Config("alembic.ini")
38
+ alembic_config.set_main_option(
39
+ "sqlalchemy.url", os.getenv("SQLALCHEMY_DATABASE_URI")
40
+ )
41
+ async with DatabaseConfig.async_engine().begin() as session:
42
+ await session.run_sync(run_upgrade, alembic_config)
43
+
44
+
45
  def run_scheduler():
46
  loop = asyncio.new_event_loop()
47
  asyncio.set_event_loop(loop)
 
53
  async def lifespan(app: FastAPI):
54
  try:
55
  logger.info("Starting up the application...")
56
+ await run_migrations()
57
  scheduler_thread = Thread(target=run_scheduler, daemon=True)
58
  scheduler_thread.start()
59
  logger.info("Application started successfully...")
 
83
  return {"response": "Service is healthy!"}
84
 
85
  app.include_router(bot_router, prefix="/api/v1")
86
+ app.include_router(user_router, prefix="/api/v1")
src/controllers/__init__.py CHANGED
@@ -1,7 +1,9 @@
1
  from ._bot_controller import BotController
 
2
 
3
  bot_router = BotController().router
 
4
 
5
- __all__ = ["bot_router"]
6
  __version__ = "0.1.0"
7
  __author__ = "Ashok Bhati"
 
1
  from ._bot_controller import BotController
2
+ from ._user_controller import UserController
3
 
4
  bot_router = BotController().router
5
+ user_router = UserController().router
6
 
7
+ __all__ = ["bot_router", "user_router"]
8
  __version__ = "0.1.0"
9
  __author__ = "Ashok Bhati"
src/controllers/_user_controller.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import APIRouter, HTTPException
3
+ from src.utils import logger
4
+ from src.services import UserService
5
+
6
+
7
+ class UserController:
8
+ def __init__(self):
9
+ self.service = UserService
10
+ self.router = APIRouter(prefix="/users", tags=["User"])
11
+ self.router.add_api_route("/", self.registerUser, methods=["POST"])
12
+
13
+ async def registerUser(self, user: dict):
14
+ logger.info(f"Received event: {user}")
15
+ try:
16
+ if not "email_id" in user:
17
+ raise HTTPException(status_code=400, detail="Missing email id")
18
+ if not "name" in user:
19
+ raise HTTPException(status_code=400, detail="Missing name")
20
+ async with self.service() as service:
21
+ user = await service.register_user(user)
22
+ return {"message": "User registered successfully", "user": user}
23
+ except HTTPException as e:
24
+ logger.warning(e)
25
+ raise e
26
+ except Exception as e:
27
+ logger.error(e)
28
+ raise HTTPException(status_code=500, detail=str(e))
src/models/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from ._base import Base
2
+ from ._user import User, ContentDeliveryFrequency
3
+ from ._slack_team import SlackTeam
4
+
5
+ __all__ = ["Base", "User", "ContentDeliveryFrequency", "SlackTeam"]
6
+ __version__ = "0.1.0"
7
+ __author__ = "Kanha Upadhyay"
src/models/_base.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from sqlalchemy.orm import declarative_base
2
+
3
+ Base = declarative_base()
src/models/_slack_team.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum as PyEnum
2
+
3
+ from sqlalchemy import Column, DateTime, Enum, Integer, String, func
4
+ from sqlalchemy.orm import relationship
5
+
6
+ from ._base import Base
7
+
8
+
9
+ class ContentDeliveryFrequency(PyEnum):
10
+ DAILY = "DAILY"
11
+ WEEKLY = "WEEKLY"
12
+ BI_WEEKLY = "BI_WEEKLY"
13
+ MONTHLY = "MONTHLY"
14
+
15
+
16
+ class SlackTeam(Base):
17
+ __tablename__ = "slack_teams"
18
+
19
+ id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
20
+ name = Column(String, nullable=False)
21
+ slack_team_id = Column(
22
+ String,
23
+ nullable=False,
24
+ )
25
+ token = Column(String, nullable=False)
26
+ created_at = Column(DateTime, nullable=False, default=func.now())
27
+
28
+ users = relationship("User", back_populates="slack_team")
src/models/_user.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum as PyEnum
2
+
3
+ from sqlalchemy import Column, DateTime, Enum, Integer, String, ForeignKey, func
4
+ from sqlalchemy.orm import relationship
5
+
6
+ from ._base import Base
7
+
8
+
9
+ class ContentDeliveryFrequency(PyEnum):
10
+ DAILY = "DAILY"
11
+ WEEKLY = "WEEKLY"
12
+ BI_WEEKLY = "BI_WEEKLY"
13
+ MONTHLY = "MONTHLY"
14
+
15
+
16
+ class User(Base):
17
+ __tablename__ = "users"
18
+
19
+ id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
20
+ name = Column(String, nullable=False)
21
+ content_delivery_frequency = Column(
22
+ Enum(ContentDeliveryFrequency),
23
+ nullable=False,
24
+ default=ContentDeliveryFrequency.WEEKLY,
25
+ )
26
+ slack_id = Column(String, nullable=True)
27
+ slack_team_id = Column(
28
+ Integer,
29
+ ForeignKey("slack_teams.id"),
30
+ nullable=True,
31
+ )
32
+ email_id = Column(String, nullable=False)
33
+ last_message_sent_at = Column(DateTime, nullable=True)
34
+ created_at = Column(DateTime, nullable=False, default=func.now())
35
+
36
+ slack_team = relationship("SlackTeam", back_populates="users")
src/repositories/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from ._base_repository import BaseRepository
2
+ from ._config import DatabaseConfig
3
+ from ._user_repository import UserRepository
4
+ from ._slack_team_repository import SlackTeamRepository
5
+
6
+ __all__ = ["DatabaseConfig", "BaseRepository", "UserRepository", "SlackTeamRepository"]
7
+ __version__ = "0.1.0"
8
+ __author__ = "Kanha Upadhyay"
src/repositories/_base_repository.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+
3
+ from sqlalchemy.future import select
4
+
5
+ from ._config import DatabaseConfig
6
+
7
+
8
+ class BaseRepository:
9
+ def __init__(self, model=None):
10
+ self.__model = model
11
+ assert self.__model is not None, "model must be defined"
12
+
13
+ @asynccontextmanager
14
+ async def get_session(self):
15
+ async with DatabaseConfig.async_session() as session:
16
+ yield session
17
+
18
+ async def execute(self, query):
19
+ async with self.get_session() as session:
20
+ result = await session.execute(query)
21
+ return result
22
+
23
+ async def create(self, kwargs: dict):
24
+ async with self.get_session() as session:
25
+ instance = self.__model(**kwargs)
26
+ session.add(instance)
27
+ await session.commit()
28
+ await session.refresh(instance)
29
+ return instance
30
+
31
+ async def get_all(
32
+ self,
33
+ page: int = 1,
34
+ page_size: int = 10,
35
+ filter_by: dict = None,
36
+ order_by: dict = None,
37
+ ):
38
+ async with self.get_session() as session:
39
+ offset = (page - 1) * page_size
40
+ query = select(self.__model).offset(offset).limit(page_size)
41
+ if filter_by:
42
+ query = query.filter_by(**filter_by)
43
+ if order_by:
44
+ for field, direction in order_by.items():
45
+ if direction == "asc":
46
+ query = query.order_by(getattr(self.__model, field).asc())
47
+ elif direction == "desc":
48
+ query = query.order_by(getattr(self.__model, field).desc())
49
+ result = await session.execute(query)
50
+ return [row for row in result.scalars()]
51
+
52
+ async def get(self, id, filter_by: dict = None):
53
+ async with self.get_session() as session:
54
+ query = select(self.__model).where(self.__model.id == id)
55
+ if filter_by:
56
+ query = query.filter_by(**filter_by)
57
+ result = await session.execute(query)
58
+ result = result.scalar()
59
+ if result:
60
+ return result
61
+ raise Exception(f"{self.__model.__name__} not found")
62
+
63
+ async def patch(self, id, **kwargs: dict):
64
+ async with self.get_session() as session:
65
+ instance = await session.get(self.__model, id)
66
+ if instance:
67
+ for key, value in kwargs.items():
68
+ setattr(instance, key, value)
69
+ await session.commit()
70
+ await session.refresh(instance)
71
+ return instance
72
+ raise Exception(f"{self.__model.__name__} not found")
73
+
74
+ async def delete(self, id):
75
+ async with self.get_session() as session:
76
+ instance = await session.get(self.__model, id)
77
+ if instance:
78
+ await session.delete(instance)
79
+ await session.commit()
80
+ return True
81
+ raise Exception(f"{self.__model.__name__} not found")
src/repositories/_config.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from contextlib import asynccontextmanager
3
+
4
+ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
5
+
6
+
7
+ class DatabaseConfig:
8
+ @classmethod
9
+ def async_engine(cls):
10
+ return create_async_engine(
11
+ os.getenv("SQLALCHEMY_DATABASE_URI"), pool_pre_ping=True, pool_recycle=3600
12
+ )
13
+
14
+ @classmethod
15
+ @asynccontextmanager
16
+ async def async_session(cls):
17
+ engine = cls.async_engine()
18
+ session_factory = async_sessionmaker(
19
+ bind=engine,
20
+ autoflush=False,
21
+ autocommit=False,
22
+ expire_on_commit=False,
23
+ )
24
+ session = session_factory()
25
+ try:
26
+ yield session
27
+ finally:
28
+ await session.close()
29
+ await engine.dispose()
src/repositories/_slack_team_repository.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from src.models import SlackTeam
2
+
3
+ from ._base_repository import BaseRepository
4
+
5
+
6
+ class SlackTeamRepository(BaseRepository):
7
+ def __init__(self):
8
+ super().__init__(model=SlackTeam)
src/repositories/_user_repository.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import select
2
+
3
+ from src.models import User
4
+
5
+ from ._base_repository import BaseRepository
6
+
7
+
8
+ class UserRepository(BaseRepository):
9
+ def __init__(self):
10
+ super().__init__(model=User)
11
+
12
+ async def get_user_by_email(self, email: str):
13
+ query = select(User).where(User.email == email)
14
+ result = await self.execute(query)
15
+ result = result.scalar_one_or_none()
16
+ return result
src/services/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
  from ._content_delivery_service import ContentDeliveryService
 
2
 
3
- __all__ = ["ContentDeliveryService"]
4
  __version__ = "0.1.0"
5
  __author__ = "Ashok Bhati"
 
1
  from ._content_delivery_service import ContentDeliveryService
2
+ from ._user_service import UserService
3
 
4
+ __all__ = ["ContentDeliveryService", "UserService"]
5
  __version__ = "0.1.0"
6
  __author__ = "Ashok Bhati"
src/services/_slack_team_service.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.repositories import SlackTeamRepository
2
+
3
+ from src.models import ContentDeliveryFrequency
4
+ import datetime
5
+
6
+
7
+ class SlackTeamService:
8
+ def __init__(self):
9
+ self.slackTeam_repository = SlackTeamRepository()
10
+
11
+ async def __aenter__(self):
12
+ return self
13
+
14
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
15
+ pass
16
+
17
+ async def register_slackTeam(self, email: str):
18
+ return await self.slackTeam_repository.create(email=email)
19
+
20
+ async def get_slackTeam_token(self, slackTeam_id):
21
+ return await self.slackTeam_repository.get(
22
+ slackTeam_id, filter_by={"token": True}
23
+ )
src/services/_user_service.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.repositories import UserRepository
2
+
3
+ from src.models import ContentDeliveryFrequency
4
+ import datetime
5
+
6
+
7
+ class UserService:
8
+ def __init__(self):
9
+ self.user_repository = UserRepository()
10
+
11
+ async def __aenter__(self):
12
+ return self
13
+
14
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
15
+ pass
16
+
17
+ async def register_user(self, user: dict) -> dict:
18
+ return await self.user_repository.create(user)
19
+
20
+ async def get_user_info_by_email(self, email: str):
21
+ return await self.user_repository.get_user_by_email(email=email)
22
+
23
+ async def update_user_slack_info(self, user_id, info):
24
+ return await self.user_repository.patch(user_id, info)
25
+
26
+ async def update_user_frequency(self, user_id, frequency: ContentDeliveryFrequency):
27
+ return await self.user_repository.patch(
28
+ user_id, {"content_delivery_frequency": frequency.value}
29
+ )
30
+
31
+ async def get_users_list(self):
32
+ return await self.user_repository.get_all()
33
+
34
+ async def get_user_last_message_time(self, user_id):
35
+ return await self.user_repository.get(
36
+ user_id, filter_by={"last_message_time": True}
37
+ )
38
+
39
+ async def update_user_last_message_time(self, user_id):
40
+ return await self.user_repository.patch(
41
+ id=user_id,
42
+ last_message_time=datetime.now(),
43
+ )
44
+
45
+ async def get_user_frequency(self, user_id):
46
+ return await self.user_repository.get(
47
+ user_id, filter_by={"content_delivery_frequency": True}
48
+ )