Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

슬랙에서 멤버 DB 채우기 #20

Merged
merged 8 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .env.dev

This file was deleted.

6 changes: 0 additions & 6 deletions .env.prod

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ node_modules
*.pickle
.DS_Store
/install-pyenv-win.ps1

*.json
*.csv
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ docker run --name waffledotcom-test \
-d mysql:latest
```

To run locally:

```
docker run -d \
-e MYSQL_ROOT_PASSWORD=local \
-e MYSQL_DATABASE=waffledotcom_local \
-e MYSQL_USER=waffledotcom-local \
-e MYSQL_PASSWORD=local \
-p 3306:3306 \
--name waffledotcom-local \
mysql:latest
```

## Convention

Expand Down
2 changes: 2 additions & 0 deletions waffledotcom/src/apps/user/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class User(DeclarativeBase):
department: Mapped[str50_default_none]
college: Mapped[str50_default_none]

generation: Mapped[str30 | None]

phone_number: Mapped[str30 | None]

github_id: Mapped[str50_default_none]
Expand Down
3 changes: 3 additions & 0 deletions waffledotcom/src/apps/user/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ def get_user_by_id(self, user_id: int) -> User | None:
def get_user_by_sso_id(self, sso_id: str) -> User | None:
return self.session.query(User).filter(User.sso_id == sso_id).first()

def get_user_by_slack_id(self, slack_id: str) -> User | None:
return self.session.query(User).filter(User.slack_id == slack_id).first()

def create_user(self, user: User) -> User:
with self.transaction:
self.session.add(user)
Expand Down
21 changes: 16 additions & 5 deletions waffledotcom/src/apps/user/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
UserCreateUpdateRequest,
UserDetailResponse,
)
from waffledotcom.src.external.slack.schema import SlackMember
from waffledotcom.src.batch.slack.schema import SlackMember


class UserService:
Expand Down Expand Up @@ -55,16 +55,27 @@ def create_users_from_slack(self, slack_members: list[SlackMember]) -> None:
slack_id=slack_member.id,
slack_email=slack_member.profile.email,
first_name=slack_member.real_name or "",
phone_number=slack_member.profile.phone or None,
last_name="",
is_member=True,
)
for slack_member in slack_members
]

try:
self.user_repository.create_users(users)
except IntegrityError as exc:
raise UserAlreadyExistsException from exc
for user in users:
assert isinstance(user.slack_id, str)
if (
created_user := self.user_repository.get_user_by_slack_id(user.slack_id)
) is None:
self.user_repository.create_user(user)
continue

created_user.slack_email = user.slack_email
created_user.phone_number = user.phone_number
created_user.image_url = user.image_url
created_user.first_name = user.first_name
created_user.last_name = user.last_name
self.user_repository.update_user(created_user)

def update_user(
self, user_id: int, request: UserCreateUpdateRequest
Expand Down
21 changes: 21 additions & 0 deletions waffledotcom/src/batch/slack/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from pydantic_settings import BaseSettings, SettingsConfigDict

from waffledotcom.src.secrets import AWSSecretManager
from waffledotcom.src.settings import settings


class SlackConfig(BaseSettings):
token: str = ""

model_config = SettingsConfigDict(
case_sensitive=False, env_prefix="SLACK_", env_file=settings.env_files
)
Comment on lines +10 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2) 만약 dotenv 아예 안 쓸 거면 여기 지워도 되지 싶습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아예 안쓰는건 아니고, .env.local, .env.test만 두려고 합니다


def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
aws_secret = AWSSecretManager()
if aws_secret.is_available():
self.token = aws_secret.get_secret("slack_token")


slack_config = SlackConfig()
49 changes: 49 additions & 0 deletions waffledotcom/src/batch/slack/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import asyncio

from fastapi import Depends
from loguru import logger
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

from waffledotcom.src.apps.user.services import UserService
from waffledotcom.src.batch.slack.config import slack_config
from waffledotcom.src.batch.slack.schema import SlackMember
from waffledotcom.src.utils.dependency_solver import DependencySolver


async def create_users_from_slack(user_service: UserService = Depends()):
client = WebClient(token=slack_config.token)
data = client.users_list().data

assert isinstance(data, dict)
Comment on lines +16 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3) 무조건 dict 인건가요? 맞으면 그냥 이렇게 한 줄로 어떤가요

Suggested change
data = client.users_list().data
assert isinstance(data, dict)
data: dict = client.users_list().data

P2) 만약 dict가 아닐 수도 있다면, assert 말고 적절한 에러를 던지고 이유를 로깅해주는 게 좋을 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slack sdk가 type hinting이 제대로 안돼있어서 pylance가 빨간줄 띄우는거같아요

image


if not data.get("ok", False):
raise SlackApiError("Slack API Error", data)

members_to_create = []
for member in data.get("members", []):
if member["is_bot"] or member["deleted"] or member["id"] == "USLACKBOT":
continue

member = SlackMember(**member)
if member.profile.phone is not None:
phone = (
member.profile.phone.replace("-", "")
.replace(" ", "")
.replace("+82", "")
)
member.profile.phone = phone

members_to_create.append(member)

user_service.create_users_from_slack(members_to_create)
logger.debug(f"Created {len(members_to_create)} users from slack")


def main():
solver = DependencySolver()
asyncio.run(solver.run(create_users_from_slack))
Comment on lines +43 to +45
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3) 이 부분 잘 이해가 안 가는데... batch 면 일정 주기마다 돌아가는 거 아닌가요?? 일회성으로 실행하는 것처럼 보이는데 부연 설명 좀 부탁합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스크립트는 일회성으로 실행하는게 맞지 않을까요
일정 주기로 돌리는건 cronjob이든 뭐든 외부에서 해주는게 맞을거같아요



if __name__ == "__main__":
main()
24 changes: 24 additions & 0 deletions waffledotcom/src/batch/slack/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations

from pydantic import BaseModel


class SlackMember(BaseModel):
id: str
real_name: str | None = None
profile: SlackMemberProfile
deleted: bool
is_bot: bool
is_email_confirmed: bool | None = None
is_admin: bool | None = None


class SlackMemberProfile(BaseModel):
first_name: str | None = None
last_name: str | None = None
email: str | None = None
phone: str | None = None
image_192: str | None = None


SlackMember.model_rebuild()
2 changes: 2 additions & 0 deletions waffledotcom/src/database/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import waffledotcom.src.apps.team.models # noqa
import waffledotcom.src.apps.user.models # noqa
7 changes: 5 additions & 2 deletions waffledotcom/src/database/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@ class DBConfig(BaseSettings):
case_sensitive=False, env_prefix="DB_", env_file=settings.env_files
)

@property
def url(self) -> str:
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
Comment on lines +20 to +21
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2) *args, **kwargs 는 없애면 어떨까요?? 나중에 필요한 필드가 있다면 그때 추가해도 될 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 근데,, 안해주면 에러가 떠요.
pydantic 내부적으로 따로 로직이 있는거같네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그

Traceback (most recent call last):
  File "/Users/user/dev/waffledotcom-server/waffledotcom/src/batch/googleform/main.py", line 39, in <module>
    main()
  File "/Users/user/dev/waffledotcom-server/waffledotcom/src/batch/googleform/main.py", line 35, in main
    asyncio.run(solver.run(merge_google_form_active_members))
  File "/Users/user/.pyenv/versions/3.11.1/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/Users/user/.pyenv/versions/3.11.1/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/.pyenv/versions/3.11.1/lib/python3.11/asyncio/base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/Users/user/dev/waffledotcom-server/waffledotcom/src/utils/dependency_solver.py", line 53, in run
    return await self.solve_command(request, dependant)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/dev/waffledotcom-server/waffledotcom/src/utils/dependency_solver.py", line 28, in solve_command
    values, errors, _1, _2, _3 = await solve_dependencies(
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/dev/waffledotcom-server/.venv/lib/python3.11/site-packages/fastapi/dependencies/utils.py", line 555, in solve_dependencies
    solved_result = await solve_dependencies(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/dev/waffledotcom-server/.venv/lib/python3.11/site-packages/fastapi/dependencies/utils.py", line 580, in solve_dependencies
    solved = await solve_generator(
             ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/dev/waffledotcom-server/.venv/lib/python3.11/site-packages/fastapi/dependencies/utils.py", line 505, in solve_generator
    return await stack.enter_async_context(cm)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/.pyenv/versions/3.11.1/lib/python3.11/contextlib.py", line 635, in enter_async_context
    result = await _enter(cm)
             ^^^^^^^^^^^^^^^^
  File "/Users/user/.pyenv/versions/3.11.1/lib/python3.11/contextlib.py", line 204, in __aenter__
    return await anext(self.gen)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/user/dev/waffledotcom-server/.venv/lib/python3.11/site-packages/fastapi/concurrency.py", line 36, in contextmanager_in_threadpool
    raise e
AttributeError: 'DBConfig' object has no attribute 'username'

aws_secrets = AWSSecretManager()
if aws_secrets.is_available():
self.username = aws_secrets.get_secret("db_username")
self.password = quote_plus(aws_secrets.get_secret("db_password"))
self.host = aws_secrets.get_secret("db_host")
self.port = int(aws_secrets.get_secret("db_port"))
self.name = aws_secrets.get_secret("db_name")

@property
def url(self) -> str:
return (
f"mysql+mysqldb://{self.username}:{self.password}"
f"@{self.host}:{self.port}/{self.name}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Initial Migration

Revision ID: f2b7ce2c8874
Revises:
Create Date: 2023-10-06 20:04:38.514945

"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "f2b7ce2c8874"
down_revision = None
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"position",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=50), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
op.create_table(
"team",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=False),
sa.Column("introduction", sa.String(length=1000), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"user",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("sso_id", sa.String(length=50), nullable=True),
sa.Column("username", sa.String(length=50), nullable=True),
sa.Column("image_url", sa.String(length=200), nullable=True),
sa.Column("first_name", sa.String(length=30), nullable=False),
sa.Column("last_name", sa.String(length=30), nullable=False),
sa.Column("department", sa.String(length=50), nullable=True),
sa.Column("college", sa.String(length=50), nullable=True),
sa.Column("phone_number", sa.String(length=30), nullable=True),
sa.Column("github_id", sa.String(length=50), nullable=True),
sa.Column("github_email", sa.String(length=50), nullable=True),
sa.Column("slack_id", sa.String(length=50), nullable=True),
sa.Column("slack_email", sa.String(length=50), nullable=True),
sa.Column("notion_email", sa.String(length=50), nullable=True),
sa.Column("apple_email", sa.String(length=50), nullable=True),
sa.Column("is_rookie", sa.Boolean(), nullable=False),
sa.Column("is_member", sa.Boolean(), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("is_admin", sa.Boolean(), nullable=False),
sa.Column("introduction", sa.String(length=1000), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("slack_id"),
sa.UniqueConstraint("sso_id"),
sa.UniqueConstraint("username"),
)
op.create_table(
"position_user_association",
sa.Column("position_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["position_id"],
["position.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("position_id", "user_id"),
)
op.create_table(
"sns_account",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=30), nullable=False),
sa.Column("url", sa.String(length=200), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"team_user_association",
sa.Column("team_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["team_id"],
["team.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("team_id", "user_id"),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("team_user_association")
op.drop_table("sns_account")
op.drop_table("position_user_association")
op.drop_table("user")
op.drop_table("team")
op.drop_table("position")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Add generation field

Revision ID: c8dea6766159
Revises: f2b7ce2c8874
Create Date: 2023-10-31 21:39:07.729126

"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "c8dea6766159"
down_revision = "f2b7ce2c8874"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("user", sa.Column("generation", sa.String(length=30), nullable=True))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("user", "generation")
# ### end Alembic commands ###
14 changes: 0 additions & 14 deletions waffledotcom/src/external/github/teams.py

This file was deleted.

Loading