angeetoile commited on
Commit
e2800b3
·
1 Parent(s): 49ef8e4

feat(database): add user model and initial migration

Browse files
Dockerfile CHANGED
@@ -19,4 +19,4 @@ USER appuser
19
 
20
  EXPOSE 7860
21
 
22
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
 
19
 
20
  EXPOSE 7860
21
 
22
+ CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
alembic.ini ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [alembic]
2
+ script_location = alembic
3
+ prepend_sys_path = .
4
+ path_separator = os
5
+ sqlalchemy.url = postgresql+psycopg://placeholder
6
+
7
+ [loggers]
8
+ keys = root,sqlalchemy,alembic
9
+
10
+ [handlers]
11
+ keys = console
12
+
13
+ [formatters]
14
+ keys = generic
15
+
16
+ [logger_root]
17
+ level = WARN
18
+ handlers = console
19
+ qualname =
20
+
21
+ [logger_sqlalchemy]
22
+ level = WARN
23
+ handlers =
24
+ qualname = sqlalchemy.engine
25
+
26
+ [logger_alembic]
27
+ level = INFO
28
+ handlers =
29
+ qualname = alembic
30
+
31
+ [handler_console]
32
+ class = StreamHandler
33
+ args = (sys.stderr,)
34
+ level = NOTSET
35
+ formatter = generic
36
+
37
+ [formatter_generic]
38
+ format = %(levelname)-5.5s [%(name)s] %(message)s
39
+ datefmt = %H:%M:%S
alembic/env.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from logging.config import fileConfig
3
+
4
+ from alembic import context
5
+ from sqlalchemy import pool
6
+ from sqlalchemy.ext.asyncio import async_engine_from_config
7
+
8
+ from app.core.config import settings
9
+ from app.db.base import Base
10
+ import app.models # noqa: F401
11
+
12
+
13
+ config = context.config
14
+
15
+ if config.config_file_name is not None:
16
+ fileConfig(config.config_file_name)
17
+
18
+ database_url = settings.sqlalchemy_database_url.replace("%", "%%")
19
+ config.set_main_option("sqlalchemy.url", database_url)
20
+
21
+ target_metadata = Base.metadata
22
+
23
+
24
+ def run_migrations_offline() -> None:
25
+ context.configure(
26
+ url=settings.sqlalchemy_database_url,
27
+ target_metadata=target_metadata,
28
+ literal_binds=True,
29
+ dialect_opts={"paramstyle": "named"},
30
+ compare_type=True,
31
+ )
32
+
33
+ with context.begin_transaction():
34
+ context.run_migrations()
35
+
36
+
37
+ def do_run_migrations(connection) -> None:
38
+ context.configure(
39
+ connection=connection,
40
+ target_metadata=target_metadata,
41
+ compare_type=True,
42
+ compare_server_default=True,
43
+ )
44
+
45
+ with context.begin_transaction():
46
+ context.run_migrations()
47
+
48
+
49
+ async def run_async_migrations() -> None:
50
+ connectable = async_engine_from_config(
51
+ config.get_section(config.config_ini_section, {}),
52
+ prefix="sqlalchemy.",
53
+ poolclass=pool.NullPool,
54
+ )
55
+
56
+ async with connectable.connect() as connection:
57
+ await connection.run_sync(do_run_migrations)
58
+
59
+ await connectable.dispose()
60
+
61
+
62
+ def run_migrations_online() -> None:
63
+ asyncio.run(run_async_migrations())
64
+
65
+
66
+ if context.is_offline_mode():
67
+ run_migrations_offline()
68
+ else:
69
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+ """
7
+
8
+ from collections.abc import Sequence
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: str | None = ${repr(down_revision)}
17
+ branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
18
+ depends_on: str | Sequence[str] | None = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ ${upgrades if upgrades else "pass"}
23
+
24
+
25
+ def downgrade() -> None:
26
+ ${downgrades if downgrades else "pass"}
alembic/versions/20260803_0001_create_users.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Create users table.
2
+
3
+ Revision ID: 20260803_0001
4
+ Revises:
5
+ """
6
+
7
+ from collections.abc import Sequence
8
+
9
+ from alembic import op
10
+ import sqlalchemy as sa
11
+ from sqlalchemy.dialects import postgresql
12
+
13
+
14
+ revision: str = "20260803_0001"
15
+ down_revision: str | None = None
16
+ branch_labels: str | Sequence[str] | None = None
17
+ depends_on: str | Sequence[str] | None = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ op.create_table(
22
+ "users",
23
+ sa.Column(
24
+ "id",
25
+ postgresql.UUID(as_uuid=True),
26
+ nullable=False,
27
+ ),
28
+ sa.Column(
29
+ "full_name",
30
+ sa.String(length=150),
31
+ nullable=False,
32
+ ),
33
+ sa.Column(
34
+ "email",
35
+ sa.String(length=320),
36
+ nullable=False,
37
+ ),
38
+ sa.Column(
39
+ "password_hash",
40
+ sa.String(length=255),
41
+ nullable=False,
42
+ ),
43
+ sa.Column(
44
+ "country_code",
45
+ sa.String(length=2),
46
+ nullable=True,
47
+ ),
48
+ sa.Column(
49
+ "preferred_language",
50
+ sa.String(length=5),
51
+ server_default="fr",
52
+ nullable=False,
53
+ ),
54
+ sa.Column(
55
+ "role",
56
+ sa.String(length=30),
57
+ server_default="user",
58
+ nullable=False,
59
+ ),
60
+ sa.Column(
61
+ "is_active",
62
+ sa.Boolean(),
63
+ server_default=sa.true(),
64
+ nullable=False,
65
+ ),
66
+ sa.Column(
67
+ "is_email_verified",
68
+ sa.Boolean(),
69
+ server_default=sa.false(),
70
+ nullable=False,
71
+ ),
72
+ sa.Column(
73
+ "onboarding_completed",
74
+ sa.Boolean(),
75
+ server_default=sa.false(),
76
+ nullable=False,
77
+ ),
78
+ sa.Column(
79
+ "created_at",
80
+ sa.DateTime(timezone=True),
81
+ server_default=sa.func.now(),
82
+ nullable=False,
83
+ ),
84
+ sa.Column(
85
+ "updated_at",
86
+ sa.DateTime(timezone=True),
87
+ server_default=sa.func.now(),
88
+ nullable=False,
89
+ ),
90
+ sa.PrimaryKeyConstraint("id"),
91
+ sa.UniqueConstraint(
92
+ "email",
93
+ name="uq_users_email",
94
+ ),
95
+ )
96
+
97
+ op.create_index(
98
+ "ix_users_email",
99
+ "users",
100
+ ["email"],
101
+ unique=False,
102
+ )
103
+
104
+ op.create_index(
105
+ "ix_users_role",
106
+ "users",
107
+ ["role"],
108
+ unique=False,
109
+ )
110
+
111
+
112
+ def downgrade() -> None:
113
+ op.drop_index(
114
+ "ix_users_role",
115
+ table_name="users",
116
+ )
117
+
118
+ op.drop_index(
119
+ "ix_users_email",
120
+ table_name="users",
121
+ )
122
+
123
+ op.drop_table("users")
alembic/versions/__init__.py ADDED
File without changes
app/models/__init__.py CHANGED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from app.models.user import User
2
+
3
+ __all__ = ["User"]
app/models/user.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from datetime import datetime
3
+
4
+ from sqlalchemy import Boolean, DateTime, Index, String, func
5
+ from sqlalchemy.dialects.postgresql import UUID
6
+ from sqlalchemy.orm import Mapped, mapped_column
7
+
8
+ from app.db.base import Base
9
+
10
+
11
+ class User(Base):
12
+ __tablename__ = "users"
13
+
14
+ id: Mapped[uuid.UUID] = mapped_column(
15
+ UUID(as_uuid=True),
16
+ primary_key=True,
17
+ default=uuid.uuid4,
18
+ )
19
+
20
+ full_name: Mapped[str] = mapped_column(
21
+ String(150),
22
+ nullable=False,
23
+ )
24
+
25
+ email: Mapped[str] = mapped_column(
26
+ String(320),
27
+ nullable=False,
28
+ unique=True,
29
+ )
30
+
31
+ password_hash: Mapped[str] = mapped_column(
32
+ String(255),
33
+ nullable=False,
34
+ )
35
+
36
+ country_code: Mapped[str | None] = mapped_column(
37
+ String(2),
38
+ nullable=True,
39
+ )
40
+
41
+ preferred_language: Mapped[str] = mapped_column(
42
+ String(5),
43
+ nullable=False,
44
+ default="fr",
45
+ server_default="fr",
46
+ )
47
+
48
+ role: Mapped[str] = mapped_column(
49
+ String(30),
50
+ nullable=False,
51
+ default="user",
52
+ server_default="user",
53
+ )
54
+
55
+ is_active: Mapped[bool] = mapped_column(
56
+ Boolean,
57
+ nullable=False,
58
+ default=True,
59
+ server_default="true",
60
+ )
61
+
62
+ is_email_verified: Mapped[bool] = mapped_column(
63
+ Boolean,
64
+ nullable=False,
65
+ default=False,
66
+ server_default="false",
67
+ )
68
+
69
+ onboarding_completed: Mapped[bool] = mapped_column(
70
+ Boolean,
71
+ nullable=False,
72
+ default=False,
73
+ server_default="false",
74
+ )
75
+
76
+ created_at: Mapped[datetime] = mapped_column(
77
+ DateTime(timezone=True),
78
+ nullable=False,
79
+ server_default=func.now(),
80
+ )
81
+
82
+ updated_at: Mapped[datetime] = mapped_column(
83
+ DateTime(timezone=True),
84
+ nullable=False,
85
+ server_default=func.now(),
86
+ onupdate=func.now(),
87
+ )
88
+
89
+ __table_args__ = (
90
+ Index("ix_users_email", "email"),
91
+ Index("ix_users_role", "role"),
92
+ )