Skip to content

Commit

Permalink
Move the withings attributes from the users table to the `withings_…
Browse files Browse the repository at this point in the history
…users` table.

In the migration file, add code to migrate the data, in addition to the generated code to migrate the schema.

Update code to read from the different objects: `User` and `WithingsUser`.
  • Loading branch information
caarmen committed May 14, 2023
1 parent 000e51d commit 068c7bb
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 27 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,10 @@ curl --location 'http://your-server/withings-notification-webhook/' \

You can find your userid in the database file:
```
sqlite3 /path/to/withingsslack.db "select slack_alias, oauth_userid from users;"
sqlite3 -header -column /path/to/withingsslack.db \
"select
slack_alias,
withings_users.oauth_userid as withings_userid
from users
left outer join withings_users on users.id = withings_users.user_id
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""move withings data to new table
Revision ID: 897c349bb45f
Revises: d1a96aed325a
Create Date: 2023-05-13 13:08:11.083878
"""
from datetime import datetime
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "897c349bb45f"
down_revision = "d1a96aed325a"
branch_labels = None
depends_on = None


def _str_to_datetime(input: str) -> datetime:
return datetime.strptime(input, "%Y-%m-%d %H:%M:%S.%f")


def _fetch_old_user_data() -> dict:
return [
{
**row._asdict(),
"oauth_expiration_date": _str_to_datetime(row.oauth_expiration_date),
}
for row in op.get_bind()
.execute(
sa.select(
sa.table("users"),
sa.text(
",".join(
[
"id as user_id",
"oauth_userid",
"oauth_access_token",
"oauth_refresh_token",
"oauth_expiration_date",
]
)
),
)
)
.all()
]


def _fetch_new_user_data():
return [
{
**row._asdict(),
"oauth_expiration_date": _str_to_datetime(row.oauth_expiration_date),
}
for row in op.get_bind()
.execute(
sa.select(
sa.table("withings_users"),
sa.text(
",".join(
[
"user_id",
"oauth_userid",
"oauth_access_token",
"oauth_refresh_token",
"oauth_expiration_date",
]
)
),
)
)
.all()
]


def upgrade() -> None:
old_user_data = _fetch_old_user_data()
# ### commands auto generated by Alembic - please adjust! ###
withings_users_table = op.create_table(
"withings_users",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("oauth_access_token", sa.String(length=40), nullable=True),
sa.Column("oauth_refresh_token", sa.String(length=40), nullable=True),
sa.Column("oauth_userid", sa.String(length=40), nullable=False),
sa.Column("oauth_expiration_date", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id"),
)
with op.batch_alter_table("withings_users", schema=None) as batch_op:
batch_op.create_index(batch_op.f("ix_withings_users_id"), ["id"], unique=False)

with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.drop_column("oauth_userid")
batch_op.drop_column("oauth_access_token")
batch_op.drop_column("oauth_expiration_date")
batch_op.drop_column("oauth_refresh_token")

# ### end Alembic commands ###

op.bulk_insert(table=withings_users_table, rows=old_user_data)


def downgrade() -> None:
new_user_data = _fetch_new_user_data()
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.add_column(
sa.Column("oauth_refresh_token", sa.VARCHAR(length=40), nullable=True)
)
batch_op.add_column(
sa.Column("oauth_expiration_date", sa.DATETIME(), nullable=True)
)
batch_op.add_column(
sa.Column("oauth_access_token", sa.VARCHAR(length=40), nullable=True)
)
batch_op.add_column(
sa.Column("oauth_userid", sa.VARCHAR(length=40), nullable=True)
)

users = sa.table(
"users",
sa.column("id"),
sa.column("oauth_userid"),
sa.column("oauth_access_token"),
sa.column("oauth_refresh_token"),
sa.column("oauth_expiration_date"),
)
for data in new_user_data:
op.get_bind().execute(
sa.update(users)
.where(users.c.id == data["user_id"])
.values({k: v for k, v in data.items() if k != "user_id"})
)

with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.alter_column("oauth_userid", nullable=False)

with op.batch_alter_table("withings_users", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_withings_users_id"))

op.drop_table("withings_users")
# ### end Alembic commands ###
53 changes: 42 additions & 11 deletions withingsslack/database/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,58 @@
from withingsslack.database import models


def get_user(db: Session, oauth_userid: str) -> models.User:
return db.query(models.User).filter(models.User.oauth_userid == oauth_userid).one()


def upsert_user(db: Session, oauth_userid: str, data: dict) -> models.User:
def get_user(db: Session, withings_oauth_userid: str) -> models.User:
"""
Return the user with the given withings oauth user id.
"""
return (
db.query(models.User)
.join(models.User.withings)
.filter(models.WithingsUser.oauth_userid == withings_oauth_userid)
.one()
)


def upsert_user(
db: Session,
withings_oauth_userid: str,
data: dict = None,
withings_data: dict = None,
) -> models.User:
try:
user = get_user(db, oauth_userid=oauth_userid)
return update_user(db, user, data)
user = get_user(db, withings_oauth_userid=withings_oauth_userid)
return update_user(db, user, data=data, withings_data=withings_data)
except NoResultFound:
return create_user(db, models.User(oauth_userid=oauth_userid, **data))
return create_user(
db,
models.User(**data),
withings_data=withings_data,
)


def create_user(db: Session, user: models.User) -> models.User:
def create_user(db: Session, user: models.User, withings_data: dict) -> models.User:
db.add(user)
db.commit()
withings_user = models.WithingsUser(
user_id=user.id,
**withings_data,
)
db.add(withings_user)
db.commit()
db.refresh(user)
return user


def update_user(db: Session, user: models.User, data: dict) -> models.User:
db.query(models.User).filter_by(id=user.id).update(data)
def update_user(
db: Session,
user: models.User,
data: dict = None,
withings_data: dict = None,
) -> models.User:
if data:
db.query(models.User).filter_by(id=user.id).update(data)
if withings_data:
db.query(models.WithingsUser).filter_by(user_id=user.id).update(withings_data)
db.commit()
db.refresh(user)
return user
12 changes: 10 additions & 2 deletions withingsslack/database/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
from sqlalchemy import String
from sqlalchemy import ForeignKey, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship

from typing import Optional

Expand All @@ -12,6 +12,14 @@ class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
slack_alias: Mapped[str] = mapped_column(unique=True, index=True)
withings: Mapped["WithingsUser"] = relationship(back_populates="user")


class WithingsUser(Base):
__tablename__ = "withings_users"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True)
user: Mapped["User"] = relationship(back_populates="withings")
oauth_access_token: Mapped[Optional[str]] = mapped_column(String(40))
oauth_refresh_token: Mapped[Optional[str]] = mapped_column(String(40))
oauth_userid: Mapped[str] = mapped_column(String(40))
Expand Down
2 changes: 1 addition & 1 deletion withingsslack/services/withings/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def get_last_weight(
) -> Optional[svc_models.WeightData]:
# https://developer.withings.com/api-reference/#tag/measure/operation/measure-getmeas
try:
user = crud.get_user(db, oauth_userid=userid)
user = crud.get_user(db, withings_oauth_userid=userid)
except NoResultFound:
logging.info(f"get_last_weight: User {userid} unknown")
return None
Expand Down
25 changes: 13 additions & 12 deletions withingsslack/services/withings/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ class MemorySettings:

@dataclasses.dataclass
class OauthFields:
oauth_userid: str
oauth_access_token: str
oauth_refresh_token: str
oauth_expiration_date: datetime

@classmethod
def parse_response_data(cls, response_data: dict) -> Self:
return cls(
oauth_userid=response_data["userid"],
oauth_access_token=response_data["access_token"],
oauth_refresh_token=response_data["refresh_token"],
oauth_expiration_date=datetime.datetime.utcnow()
Expand Down Expand Up @@ -73,22 +75,21 @@ def fetch_token(db: Session, state: str, code: str) -> db_models.User:
oauth_userid = response_data["userid"]
oauth_fields = OauthFields.parse_response_data(response_data)
user = crud.upsert_user(
db, oauth_userid=oauth_userid, data={"slack_alias": slack_alias}
)
crud.update_user(
db,
user=user,
data={
**dataclasses.asdict(oauth_fields),
},
withings_oauth_userid=oauth_userid,
data={"slack_alias": slack_alias},
withings_data=dataclasses.asdict(oauth_fields),
)
return user


def get_access_token(db: Session, user: db_models.User) -> str:
if user.oauth_expiration_date < datetime.datetime.utcnow():
if (
not user.withings.oauth_expiration_date
or user.withings.oauth_expiration_date < datetime.datetime.utcnow()
):
refresh_token(db, user)
return user.oauth_access_token
return user.withings.oauth_access_token


def refresh_token(db: Session, user: db_models.User) -> str:
Expand All @@ -99,7 +100,7 @@ def refresh_token(db: Session, user: db_models.User) -> str:
"action": "requesttoken",
"client_id": settings.withings_client_id,
"grant_type": "refresh_token",
"refresh_token": user.oauth_refresh_token,
"refresh_token": user.withings.oauth_refresh_token,
**signing.sign_action("requesttoken"),
},
)
Expand All @@ -108,6 +109,6 @@ def refresh_token(db: Session, user: db_models.User) -> str:
user = crud.update_user(
db,
user=user,
data=dataclasses.asdict(oauth_fields),
withings_data=dataclasses.asdict(oauth_fields),
)
return user.oauth_access_token
return user.withings.oauth_access_token

0 comments on commit 068c7bb

Please sign in to comment.