linhnguyen02 commited on
Commit
d3530f3
·
1 Parent(s): d88c2c1

init commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +17 -0
  2. __pycache__/env.cpython-313.pyc +0 -0
  3. __pycache__/main.cpython-313.pyc +0 -0
  4. alembic.ini +147 -0
  5. alembic/README +1 -0
  6. alembic/__pycache__/env.cpython-313.pyc +0 -0
  7. alembic/env.py +87 -0
  8. alembic/script.py.mako +28 -0
  9. alembic/versions/7531e2b3a772_init_database.py +100 -0
  10. alembic/versions/__pycache__/7531e2b3a772_init_database.cpython-313.pyc +0 -0
  11. env.py +25 -0
  12. main.py +23 -0
  13. requirements.txt +135 -0
  14. src/dtos/__pycache__/user.cpython-313.pyc +0 -0
  15. src/dtos/user.py +11 -0
  16. src/interfaces/__pycache__/auth.cpython-313.pyc +0 -0
  17. src/interfaces/__pycache__/question.cpython-313.pyc +0 -0
  18. src/interfaces/__pycache__/user.cpython-313.pyc +0 -0
  19. src/interfaces/auth.py +15 -0
  20. src/interfaces/question.py +12 -0
  21. src/interfaces/user.py +19 -0
  22. src/loaders/__pycache__/app.cpython-313.pyc +0 -0
  23. src/loaders/__pycache__/database.cpython-313.pyc +0 -0
  24. src/loaders/database.py +25 -0
  25. src/middlewares/__pycache__/base.cpython-313.pyc +0 -0
  26. src/middlewares/__pycache__/logging.cpython-313.pyc +0 -0
  27. src/middlewares/authenticate.py +25 -0
  28. src/models/__init__.py +5 -0
  29. src/models/__pycache__/__init__.cpython-313.pyc +0 -0
  30. src/models/__pycache__/base.cpython-313.pyc +0 -0
  31. src/models/__pycache__/choice.cpython-313.pyc +0 -0
  32. src/models/__pycache__/comment.cpython-313.pyc +0 -0
  33. src/models/__pycache__/question.cpython-313.pyc +0 -0
  34. src/models/__pycache__/rating.cpython-313.pyc +0 -0
  35. src/models/__pycache__/user.cpython-313.pyc +0 -0
  36. src/models/base.py +3 -0
  37. src/models/choice.py +15 -0
  38. src/models/comment.py +16 -0
  39. src/models/paragraph.py +0 -0
  40. src/models/question.py +24 -0
  41. src/models/rating.py +15 -0
  42. src/models/user.py +22 -0
  43. src/repositories/__pycache__/auth.cpython-313.pyc +0 -0
  44. src/repositories/__pycache__/base.cpython-313.pyc +0 -0
  45. src/repositories/__pycache__/user.cpython-313.pyc +0 -0
  46. src/repositories/auth.py +12 -0
  47. src/repositories/base.py +32 -0
  48. src/repositories/user.py +73 -0
  49. src/routers/public/__pycache__/auth.cpython-313.pyc +0 -0
  50. src/routers/public/__pycache__/public.cpython-313.pyc +0 -0
.env ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app
2
+ PORT=8001
3
+
4
+ # database
5
+ DB_HOST=127.0.0.1
6
+ DB_PORT=3306
7
+ DB_USER=root
8
+ DB_PASSWORD=password
9
+ DB_DATABASE=mini-project
10
+ POOL_SIZE=8
11
+ MAX_OVERFLOW=32
12
+ POOL_RECYCLE=64
13
+
14
+ # jwt
15
+ JWT_EXPIRATION_DELTA=24
16
+ JWT_ALGORITHM=HS256
17
+ JWT_SECRET=key123456
__pycache__/env.cpython-313.pyc ADDED
Binary file (1.24 kB). View file
 
__pycache__/main.cpython-313.pyc ADDED
Binary file (1.34 kB). View file
 
alembic.ini ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts.
5
+ # this is typically a path given in POSIX (e.g. forward slashes)
6
+ # format, relative to the token %(here)s which refers to the location of this
7
+ # ini file
8
+ script_location = %(here)s/alembic
9
+
10
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
11
+ # Uncomment the line below if you want the files to be prepended with date and time
12
+ # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
13
+ # for all available tokens
14
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
15
+
16
+ # sys.path path, will be prepended to sys.path if present.
17
+ # defaults to the current working directory. for multiple paths, the path separator
18
+ # is defined by "path_separator" below.
19
+ prepend_sys_path = .
20
+
21
+
22
+ # timezone to use when rendering the date within the migration file
23
+ # as well as the filename.
24
+ # If specified, requires the tzdata library which can be installed by adding
25
+ # `alembic[tz]` to the pip requirements.
26
+ # string value is passed to ZoneInfo()
27
+ # leave blank for localtime
28
+ # timezone =
29
+
30
+ # max length of characters to apply to the "slug" field
31
+ # truncate_slug_length = 40
32
+
33
+ # set to 'true' to run the environment during
34
+ # the 'revision' command, regardless of autogenerate
35
+ # revision_environment = false
36
+
37
+ # set to 'true' to allow .pyc and .pyo files without
38
+ # a source .py file to be detected as revisions in the
39
+ # versions/ directory
40
+ # sourceless = false
41
+
42
+ # version location specification; This defaults
43
+ # to <script_location>/versions. When using multiple version
44
+ # directories, initial revisions must be specified with --version-path.
45
+ # The path separator used here should be the separator specified by "path_separator"
46
+ # below.
47
+ # version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
48
+
49
+ # path_separator; This indicates what character is used to split lists of file
50
+ # paths, including version_locations and prepend_sys_path within configparser
51
+ # files such as alembic.ini.
52
+ # The default rendered in new alembic.ini files is "os", which uses os.pathsep
53
+ # to provide os-dependent path splitting.
54
+ #
55
+ # Note that in order to support legacy alembic.ini files, this default does NOT
56
+ # take place if path_separator is not present in alembic.ini. If this
57
+ # option is omitted entirely, fallback logic is as follows:
58
+ #
59
+ # 1. Parsing of the version_locations option falls back to using the legacy
60
+ # "version_path_separator" key, which if absent then falls back to the legacy
61
+ # behavior of splitting on spaces and/or commas.
62
+ # 2. Parsing of the prepend_sys_path option falls back to the legacy
63
+ # behavior of splitting on spaces, commas, or colons.
64
+ #
65
+ # Valid values for path_separator are:
66
+ #
67
+ # path_separator = :
68
+ # path_separator = ;
69
+ # path_separator = space
70
+ # path_separator = newline
71
+ #
72
+ # Use os.pathsep. Default configuration used for new projects.
73
+ path_separator = os
74
+
75
+ # set to 'true' to search source files recursively
76
+ # in each "version_locations" directory
77
+ # new in Alembic version 1.10
78
+ # recursive_version_locations = false
79
+
80
+ # the output encoding used when revision files
81
+ # are written from script.py.mako
82
+ # output_encoding = utf-8
83
+
84
+ # database URL. This is consumed by the user-maintained env.py script only.
85
+ # other means of configuring database URLs may be customized within the env.py
86
+ # file.
87
+ sqlalchemy.url = mysql+pymysql://root:password@localhost:3306/mini-project
88
+
89
+
90
+ [post_write_hooks]
91
+ # post_write_hooks defines scripts or Python functions that are run
92
+ # on newly generated revision scripts. See the documentation for further
93
+ # detail and examples
94
+
95
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
96
+ # hooks = black
97
+ # black.type = console_scripts
98
+ # black.entrypoint = black
99
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
100
+
101
+ # lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
102
+ # hooks = ruff
103
+ # ruff.type = module
104
+ # ruff.module = ruff
105
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
106
+
107
+ # Alternatively, use the exec runner to execute a binary found on your PATH
108
+ # hooks = ruff
109
+ # ruff.type = exec
110
+ # ruff.executable = ruff
111
+ # ruff.options = check --fix REVISION_SCRIPT_FILENAME
112
+
113
+ # Logging configuration. This is also consumed by the user-maintained
114
+ # env.py script only.
115
+ [loggers]
116
+ keys = root,sqlalchemy,alembic
117
+
118
+ [handlers]
119
+ keys = console
120
+
121
+ [formatters]
122
+ keys = generic
123
+
124
+ [logger_root]
125
+ level = WARNING
126
+ handlers = console
127
+ qualname =
128
+
129
+ [logger_sqlalchemy]
130
+ level = WARNING
131
+ handlers =
132
+ qualname = sqlalchemy.engine
133
+
134
+ [logger_alembic]
135
+ level = INFO
136
+ handlers =
137
+ qualname = alembic
138
+
139
+ [handler_console]
140
+ class = StreamHandler
141
+ args = (sys.stderr,)
142
+ level = NOTSET
143
+ formatter = generic
144
+
145
+ [formatter_generic]
146
+ format = %(levelname)-5.5s [%(name)s] %(message)s
147
+ datefmt = %H:%M:%S
alembic/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Generic single-database configuration.
alembic/__pycache__/env.cpython-313.pyc ADDED
Binary file (2.84 kB). View file
 
alembic/env.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+
3
+ from sqlalchemy import engine_from_config
4
+ from sqlalchemy import pool
5
+
6
+ from alembic import context
7
+
8
+ # this is the Alembic Config object, which provides
9
+ # access to the values within the .ini file in use.
10
+ config = context.config
11
+
12
+ # Interpret the config file for Python logging.
13
+ # This line sets up loggers basically.
14
+ if config.config_file_name is not None:
15
+ fileConfig(config.config_file_name)
16
+
17
+ # add your model's MetaData object here
18
+ # for 'autogenerate' support
19
+ # from myapp import mymodel
20
+ # target_metadata = mymodel.Base.metadata
21
+
22
+ from src.models.base import BaseModel
23
+ from src.models.user import User
24
+ from src.models.question import Question
25
+ from src.models.comment import Comment
26
+ from src.models.rating import Rating
27
+ from src.models.choice import Choice
28
+
29
+ target_metadata = BaseModel.metadata
30
+
31
+
32
+ # other values from the config, defined by the needs of env.py,
33
+ # can be acquired:
34
+ # my_important_option = config.get_main_option("my_important_option")
35
+ # ... etc.
36
+
37
+
38
+ def run_migrations_offline() -> None:
39
+ """Run migrations in 'offline' mode.
40
+
41
+ This configures the context with just a URL
42
+ and not an Engine, though an Engine is acceptable
43
+ here as well. By skipping the Engine creation
44
+ we don't even need a DBAPI to be available.
45
+
46
+ Calls to context.execute() here emit the given string to the
47
+ script output.
48
+
49
+ """
50
+ url = config.get_main_option("sqlalchemy.url")
51
+ context.configure(
52
+ url=url,
53
+ target_metadata=target_metadata,
54
+ literal_binds=True,
55
+ dialect_opts={"paramstyle": "named"},
56
+ )
57
+
58
+ with context.begin_transaction():
59
+ context.run_migrations()
60
+
61
+
62
+ def run_migrations_online() -> None:
63
+ """Run migrations in 'online' mode.
64
+
65
+ In this scenario we need to create an Engine
66
+ and associate a connection with the context.
67
+
68
+ """
69
+ connectable = engine_from_config(
70
+ config.get_section(config.config_ini_section, {}),
71
+ prefix="sqlalchemy.",
72
+ poolclass=pool.NullPool,
73
+ )
74
+
75
+ with connectable.connect() as connection:
76
+ context.configure(
77
+ connection=connection, target_metadata=target_metadata
78
+ )
79
+
80
+ with context.begin_transaction():
81
+ context.run_migrations()
82
+
83
+
84
+ if context.is_offline_mode():
85
+ run_migrations_offline()
86
+ else:
87
+ run_migrations_online()
alembic/script.py.mako ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ ${upgrades if upgrades else "pass"}
24
+
25
+
26
+ def downgrade() -> None:
27
+ """Downgrade schema."""
28
+ ${downgrades if downgrades else "pass"}
alembic/versions/7531e2b3a772_init_database.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """init database
2
+
3
+ Revision ID: 7531e2b3a772
4
+ Revises:
5
+ Create Date: 2025-10-14 00:42:13.032368
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 = '7531e2b3a772'
16
+ down_revision: Union[str, Sequence[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
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.create_table('users',
25
+ sa.Column('id', sa.Integer(), nullable=False),
26
+ sa.Column('username', sa.String(length=50), nullable=False),
27
+ sa.Column('email', sa.String(length=100), nullable=False),
28
+ sa.Column('password', sa.String(length=100), nullable=False),
29
+ sa.Column('avatar_url', sa.Text(), nullable=True),
30
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
31
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
32
+ sa.PrimaryKeyConstraint('id')
33
+ )
34
+ op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
35
+ op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
36
+ op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
37
+ op.create_table('questions',
38
+ sa.Column('id', sa.Integer(), nullable=False),
39
+ sa.Column('user_id', sa.Integer(), nullable=False),
40
+ sa.Column('topic', sa.String(length=100), nullable=False),
41
+ sa.Column('context', sa.Text(), nullable=False),
42
+ sa.Column('question_text', sa.Text(), nullable=False),
43
+ sa.Column('correct_choice', sa.String(length=255), nullable=False),
44
+ sa.Column('tags', sa.Text(), nullable=True),
45
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
46
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
47
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
48
+ sa.PrimaryKeyConstraint('id')
49
+ )
50
+ op.create_index(op.f('ix_questions_id'), 'questions', ['id'], unique=False)
51
+ op.create_table('choices',
52
+ sa.Column('id', sa.Integer(), nullable=False),
53
+ sa.Column('question_id', sa.Integer(), nullable=False),
54
+ sa.Column('choice_text', sa.String(length=255), nullable=False),
55
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
56
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
57
+ sa.ForeignKeyConstraint(['question_id'], ['questions.id'], ),
58
+ sa.PrimaryKeyConstraint('id')
59
+ )
60
+ op.create_index(op.f('ix_choices_id'), 'choices', ['id'], unique=False)
61
+ op.create_table('comments',
62
+ sa.Column('id', sa.Integer(), nullable=False),
63
+ sa.Column('question_id', sa.Integer(), nullable=False),
64
+ sa.Column('user_id', sa.Integer(), nullable=False),
65
+ sa.Column('comment_text', sa.Text(), nullable=False),
66
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
67
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
68
+ sa.ForeignKeyConstraint(['question_id'], ['questions.id'], ),
69
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
70
+ sa.PrimaryKeyConstraint('id')
71
+ )
72
+ op.create_index(op.f('ix_comments_id'), 'comments', ['id'], unique=False)
73
+ op.create_table('ratings',
74
+ sa.Column('user_id', sa.Integer(), nullable=False),
75
+ sa.Column('question_id', sa.Integer(), nullable=False),
76
+ sa.Column('rating_value', sa.Integer(), nullable=False),
77
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
78
+ sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
79
+ sa.ForeignKeyConstraint(['question_id'], ['questions.id'], ondelete='CASCADE'),
80
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
81
+ sa.PrimaryKeyConstraint('user_id', 'question_id')
82
+ )
83
+ # ### end Alembic commands ###
84
+
85
+
86
+ def downgrade() -> None:
87
+ """Downgrade schema."""
88
+ # ### commands auto generated by Alembic - please adjust! ###
89
+ op.drop_table('ratings')
90
+ op.drop_index(op.f('ix_comments_id'), table_name='comments')
91
+ op.drop_table('comments')
92
+ op.drop_index(op.f('ix_choices_id'), table_name='choices')
93
+ op.drop_table('choices')
94
+ op.drop_index(op.f('ix_questions_id'), table_name='questions')
95
+ op.drop_table('questions')
96
+ op.drop_index(op.f('ix_users_username'), table_name='users')
97
+ op.drop_index(op.f('ix_users_id'), table_name='users')
98
+ op.drop_index(op.f('ix_users_email'), table_name='users')
99
+ op.drop_table('users')
100
+ # ### end Alembic commands ###
alembic/versions/__pycache__/7531e2b3a772_init_database.cpython-313.pyc ADDED
Binary file (8.11 kB). View file
 
env.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ config = {
7
+ "app": {
8
+ 'port': os.getenv("PORT"),
9
+ },
10
+ "db": {
11
+ "host": os.getenv("DB_HOST"),
12
+ "port": os.getenv("DB_PORT"),
13
+ "user": os.getenv("DB_USER"),
14
+ "password": os.getenv("DB_PASSWORD"),
15
+ "database": os.getenv("DB_DATABASE"),
16
+ "pool_size": int(os.getenv("POOL_SIZE")) | 8,
17
+ "max_overflow": int(os.getenv("MAX_OVERFLOW")) | 16,
18
+ "pool_recycle": int(os.getenv("POOL_RECYCLE")),
19
+ },
20
+ "jwt": {
21
+ "expired_in": int(os.getenv("JWT_EXPIRATION_DELTA")) | 24, # hour
22
+ "algorithm": os.getenv("JWT_ALGORITHM"),
23
+ "secret_key": os.getenv("JWT_SECRET"),
24
+ }
25
+ }
main.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, HTTPException
2
+
3
+ from src.routers.public.public import router
4
+ from src.utils.response import handler_error
5
+
6
+ app = FastAPI()
7
+ @app.exception_handler(Exception)
8
+ async def exception_handler(request: Request, exc: Exception):
9
+ return handler_error(exc)
10
+
11
+ @app.exception_handler(HTTPException)
12
+ async def http_exception_handler(request: Request, exc: HTTPException):
13
+ return handler_error(exc)
14
+
15
+ @app.get('/check-health')
16
+ def check_health():
17
+ return {"status": "ok"}
18
+
19
+ app.include_router(router)
20
+
21
+ if __name__ == "__main__":
22
+ import uvicorn
23
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
requirements.txt ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiomysql==0.2.0
2
+ annotated-types==0.7.0
3
+ anyio==4.11.0
4
+ bcrypt==5.0.0
5
+ beautifulsoup4==4.14.2
6
+ blis==1.3.0
7
+ cachetools==6.2.0
8
+ catalogue==2.0.10
9
+ certifi==2025.10.5
10
+ cffi==2.0.0
11
+ charset-normalizer==3.4.3
12
+ click==8.3.0
13
+ cloudpathlib==0.22.0
14
+ colorama==0.4.6
15
+ coloredlogs==15.0.1
16
+ confection==0.1.5
17
+ cryptography==46.0.2
18
+ curated-tokenizers==0.0.9
19
+ curated-transformers==0.1.1
20
+ cymem==2.0.11
21
+ deep-translator==1.11.4
22
+ dnspython==2.8.0
23
+ email-validator==2.3.0
24
+ exceptiongroup==1.3.0
25
+ fastapi==0.118.0
26
+ fastapi-cli==0.0.13
27
+ fastapi-cloud-cli==0.3.0
28
+ fastt5==0.0.5
29
+ filelock==3.19.1
30
+ flatbuffers==25.9.23
31
+ fsspec==2025.9.0
32
+ gdown==5.2.0
33
+ google-api-core==2.25.2
34
+ google-auth==2.41.1
35
+ google-cloud-core==2.4.3
36
+ google-cloud-storage==3.4.0
37
+ google-crc32c==1.7.1
38
+ google-resumable-media==2.7.2
39
+ googleapis-common-protos==1.70.0
40
+ greenlet==3.2.4
41
+ h11==0.16.0
42
+ httpcore==1.0.9
43
+ httptools==0.6.4
44
+ httpx==0.28.1
45
+ huggingface-hub==0.35.3
46
+ humanfriendly==10.0
47
+ idna==3.10
48
+ iniconfig==2.1.0
49
+ Jinja2==3.1.6
50
+ joblib==1.5.2
51
+ jwt==1.4.0
52
+ keybert==0.9.0
53
+ keyphrase-vectorizers==0.0.13
54
+ langcodes==3.5.0
55
+ language_data==1.3.0
56
+ marisa-trie==1.3.1
57
+ markdown-it-py==4.0.0
58
+ MarkupSafe==3.0.3
59
+ mdurl==0.1.2
60
+ ml_dtypes==0.5.3
61
+ mpmath==1.3.0
62
+ murmurhash==1.0.13
63
+ networkx==3.4.2
64
+ nltk==3.9.2
65
+ numpy==2.2.6
66
+ onnx==1.19.0
67
+ onnxruntime==1.23.0
68
+ packaging==25.0
69
+ pillow==11.3.0
70
+ pluggy==1.6.0
71
+ preshed==3.0.10
72
+ progress==1.6.1
73
+ proto-plus==1.26.1
74
+ protobuf==6.32.1
75
+ psutil==7.1.0
76
+ pyasn1==0.6.1
77
+ pyasn1_modules==0.4.2
78
+ pycparser==2.23
79
+ pydantic==2.11.10
80
+ pydantic_core==2.33.2
81
+ Pygments==2.19.2
82
+ PyMySQL==1.1.2
83
+ PyPDF2==3.0.1
84
+ pyreadline3==3.5.4
85
+ PySocks==1.7.1
86
+ pytesseract==0.3.13
87
+ pytest==8.4.2
88
+ python-dotenv==1.1.1
89
+ python-multipart==0.0.20
90
+ PyYAML==6.0.3
91
+ regex==2025.9.18
92
+ requests==2.32.5
93
+ rich==14.1.0
94
+ rich-toolkit==0.15.1
95
+ rignore==0.7.0
96
+ rsa==4.9.1
97
+ safetensors==0.6.2
98
+ scikit-learn==1.7.2
99
+ scipy==1.15.3
100
+ sense2vec==2.0.2
101
+ sentence-transformers==5.1.1
102
+ sentencepiece==0.2.1
103
+ sentry-sdk==2.39.0
104
+ shellingham==1.5.4
105
+ sklearn==0.0
106
+ smart_open==7.3.1
107
+ sniffio==1.3.1
108
+ soupsieve==2.8
109
+ spacy==3.8.7
110
+ spacy-alignments==0.9.2
111
+ spacy-curated-transformers==0.3.1
112
+ spacy-legacy==3.0.12
113
+ spacy-loggers==1.0.5
114
+ spacy-transformers==1.3.9
115
+ SQLAlchemy==2.0.43
116
+ srsly==2.5.1
117
+ starlette==0.48.0
118
+ sympy==1.14.0
119
+ thinc==8.3.6
120
+ threadpoolctl==3.6.0
121
+ tokenizers==0.21.4
122
+ tomli==2.2.1
123
+ torch==2.8.0
124
+ tqdm==4.67.1
125
+ transformers==4.49.0
126
+ typer==0.19.2
127
+ typing-inspection==0.4.2
128
+ typing_extensions==4.15.0
129
+ urllib3==2.5.0
130
+ uvicorn==0.37.0
131
+ wasabi==1.1.3
132
+ watchfiles==1.1.0
133
+ weasel==0.4.1
134
+ websockets==15.0.1
135
+ wrapt==1.17.3
src/dtos/__pycache__/user.cpython-313.pyc ADDED
Binary file (572 Bytes). View file
 
src/dtos/user.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class UserDto(BaseModel):
5
+ id: int
6
+ username: str
7
+ email: str
8
+
9
+ model_config = {
10
+ "from_attributes": True,
11
+ }
src/interfaces/__pycache__/auth.cpython-313.pyc ADDED
Binary file (1.18 kB). View file
 
src/interfaces/__pycache__/question.cpython-313.pyc ADDED
Binary file (913 Bytes). View file
 
src/interfaces/__pycache__/user.cpython-313.pyc ADDED
Binary file (1.26 kB). View file
 
src/interfaces/auth.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+
3
+ class ILogin(BaseModel):
4
+ username: str
5
+ password: str
6
+
7
+ class IPagination(BaseModel):
8
+ paging: int = Field(1, ge=1)
9
+ limit: int = Field(10, ge=1)
10
+ sort_by: str = "id",
11
+ sort_order: str = 'asc',
12
+
13
+ @property
14
+ def offset(self):
15
+ return (self.page - 1) * self.limit
src/interfaces/question.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import Optional
3
+
4
+ class ModelInput(BaseModel):
5
+ """General request model structure for flutter incoming req."""
6
+ user_id: Optional[str] = None
7
+ context: str
8
+ name: str
9
+
10
+ class ICreateQuestion(BaseModel):
11
+ context: str
12
+ name: str
src/interfaces/user.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+
3
+ from pydantic import BaseModel, EmailStr
4
+ from typing import Optional
5
+
6
+ class ICreateUser(BaseModel):
7
+ username: str
8
+ email: EmailStr
9
+ password: str
10
+
11
+ class IUpdateUser(BaseModel):
12
+ username: str
13
+ email: EmailStr
14
+ password: Optional[str]
15
+
16
+ class IFilterUser(BaseModel):
17
+ username: Optional[str] = None
18
+ email: Optional[EmailStr] = None
19
+ username_or_email: Optional[str] = None
src/loaders/__pycache__/app.cpython-313.pyc ADDED
Binary file (506 Bytes). View file
 
src/loaders/__pycache__/database.cpython-313.pyc ADDED
Binary file (1.14 kB). View file
 
src/loaders/database.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from env import config
2
+
3
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
4
+
5
+ database_url = f"mysql+aiomysql://{config['db']['user']}:{config['db']['password']}@{config['db']['host']}:{config['db']['port']}/{config['db']['database']}"
6
+ engine = create_async_engine(
7
+ database_url,
8
+ pool_size=config['db']['pool_size'],
9
+ max_overflow=0,
10
+ pool_recycle=3600,
11
+ echo=False,
12
+ future=True,
13
+ )
14
+ SessionLocal = async_sessionmaker(
15
+ engine,
16
+ expire_on_commit=False,
17
+ class_=AsyncSession
18
+ )
19
+
20
+ def get_db():
21
+ db = SessionLocal()
22
+ try:
23
+ yield db
24
+ finally:
25
+ db.close()
src/middlewares/__pycache__/base.cpython-313.pyc ADDED
Binary file (751 Bytes). View file
 
src/middlewares/__pycache__/logging.cpython-313.pyc ADDED
Binary file (1.37 kB). View file
 
src/middlewares/authenticate.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Request, HTTPException, Depends
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+
4
+ import jwt
5
+
6
+ from env import config
7
+ from src.loaders.database import get_db
8
+ from src.services.user import get_user_service
9
+
10
+ async def authenticate(request: Request, db: AsyncSession = Depends(get_db), user_service = Depends(get_user_service)):
11
+ auth_header = request.headers.get("Authorization")
12
+ if not auth_header or not auth_header.startswith("Bearer"):
13
+ raise HTTPException(status_code=401, detail="token_invalid")
14
+
15
+ token = auth_header.split("Bearer")[1].strip()
16
+ payload = jwt.decode(token, config["jwt"]["secret_key"], algorithm=config["jwt"]["algorithm"])
17
+ user_id = payload.get("id")
18
+ if user_id is None:
19
+ raise HTTPException(status_code=401, detail="token_invalid")
20
+
21
+ user = await user_service.find_by_pk(db, user_id)
22
+ if user is None:
23
+ raise HTTPException(status_code=401, detail="token_invalid")
24
+ request.state.user = user
25
+
src/models/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from .choice import Choice
2
+ from .comment import Comment
3
+ from .question import Question
4
+ from .rating import Rating
5
+ from .user import User
src/models/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (347 Bytes). View file
 
src/models/__pycache__/base.cpython-313.pyc ADDED
Binary file (252 Bytes). View file
 
src/models/__pycache__/choice.cpython-313.pyc ADDED
Binary file (1.23 kB). View file
 
src/models/__pycache__/comment.cpython-313.pyc ADDED
Binary file (1.28 kB). View file
 
src/models/__pycache__/question.cpython-313.pyc ADDED
Binary file (1.71 kB). View file
 
src/models/__pycache__/rating.cpython-313.pyc ADDED
Binary file (1.23 kB). View file
 
src/models/__pycache__/user.cpython-313.pyc ADDED
Binary file (1.38 kB). View file
 
src/models/base.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from sqlalchemy.ext.declarative import declarative_base
2
+
3
+ BaseModel = declarative_base()
src/models/choice.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # models.py
2
+ from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, func
3
+ from sqlalchemy.orm import relationship
4
+ from src.models.base import BaseModel
5
+
6
+ class Choice(BaseModel):
7
+ __tablename__ = "choices"
8
+
9
+ id = Column(Integer, primary_key=True, index=True)
10
+ question_id = Column(Integer, ForeignKey("questions.id"), nullable=False)
11
+ choice_text = Column(String(255), nullable=False)
12
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
13
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
14
+
15
+ question = relationship("Question", back_populates="choices")
src/models/comment.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # models.py
2
+ from sqlalchemy import Column, Integer, ForeignKey, Text, DateTime, func
3
+ from sqlalchemy.orm import relationship
4
+ from src.models.base import BaseModel
5
+
6
+ class Comment(BaseModel):
7
+ __tablename__ = "comments"
8
+
9
+ id = Column(Integer, primary_key=True, index=True)
10
+ question_id = Column(Integer, ForeignKey("questions.id"), nullable=False)
11
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
12
+ comment_text = Column(Text, nullable=False)
13
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
14
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
15
+
16
+ question = relationship("Question", back_populates="comments")
src/models/paragraph.py ADDED
File without changes
src/models/question.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # models.py
2
+ from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, Text, Date, DateTime, func
3
+ from sqlalchemy.orm import relationship
4
+ from src.models.base import BaseModel
5
+ from datetime import datetime
6
+
7
+ class Question(BaseModel):
8
+ __tablename__ = "questions"
9
+
10
+ id = Column(Integer, primary_key=True, index=True)
11
+ user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
12
+ topic = Column(String(100), nullable=False)
13
+ context = Column(Text, nullable=False)
14
+ question_text = Column(Text, nullable=False)
15
+ correct_choice = Column(String(255), nullable=False)
16
+ tags = Column(Text, nullable=True)
17
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
18
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
19
+
20
+
21
+ user = relationship("User", back_populates="questions")
22
+ choices = relationship("Choice", back_populates="question", cascade="all, delete", passive_deletes=True)
23
+ comments = relationship("Comment", back_populates="question", cascade="all, delete", passive_deletes=True)
24
+ ratings = relationship("Rating", back_populates="question", cascade="all, delete", passive_deletes=True)
src/models/rating.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # models.py
2
+ from sqlalchemy import Column, Integer, ForeignKey, DateTime, func
3
+ from sqlalchemy.orm import relationship
4
+ from src.models.base import BaseModel
5
+
6
+ class Rating(BaseModel):
7
+ __tablename__ = "ratings"
8
+
9
+ user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True)
10
+ question_id = Column(Integer, ForeignKey("questions.id", ondelete="CASCADE"), primary_key=True)
11
+ rating_value = Column(Integer, nullable=False)
12
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
13
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
14
+
15
+ question = relationship("Question", back_populates="ratings")
src/models/user.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # models.py
2
+ from sqlalchemy import Column, Integer, String, Text, DateTime, func
3
+ from sqlalchemy.orm import relationship
4
+ from src.models.base import BaseModel
5
+
6
+ class User(BaseModel):
7
+ __tablename__ = "users"
8
+
9
+ id = Column(Integer, primary_key=True, index=True)
10
+ username = Column(String(50), unique=True, nullable=False, index=True)
11
+ email = Column(String(100), unique=True, nullable=False, index=True)
12
+ password = Column(String(100), nullable=False)
13
+ avatar_url = Column(Text, nullable=True)
14
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
15
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
16
+
17
+ questions = relationship(
18
+ "Question",
19
+ back_populates="user",
20
+ cascade="all, delete",
21
+ passive_deletes=True
22
+ )
src/repositories/__pycache__/auth.cpython-313.pyc ADDED
Binary file (1.21 kB). View file
 
src/repositories/__pycache__/base.cpython-313.pyc ADDED
Binary file (1.57 kB). View file
 
src/repositories/__pycache__/user.cpython-313.pyc ADDED
Binary file (5.32 kB). View file
 
src/repositories/auth.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import select
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+
4
+
5
+ from src.interfaces.user import *
6
+ from src.models.user import User
7
+
8
+ class AuthRepository():
9
+ async def find_by_username(self, db: AsyncSession, username: str) -> Optional[User]:
10
+ query = select(User).where(User.username == username)
11
+ result = await db.execute(query)
12
+ return await result.scalar_one_or_none()
src/repositories/base.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class ICrudRepository(ABC):
5
+ @abstractmethod
6
+ def store(self, data):
7
+ pass
8
+
9
+ @abstractmethod
10
+ def update(self, entity, data):
11
+ pass
12
+
13
+ @abstractmethod
14
+ def delete(self, entity):
15
+ pass
16
+
17
+ @abstractmethod
18
+ def find_by_pk(self, entity_id):
19
+ pass
20
+
21
+ @abstractmethod
22
+ def get_one(self, filter_data):
23
+ pass
24
+
25
+ @abstractmethod
26
+ def get_many(self, paging, filter_data):
27
+ pass
28
+
29
+ @abstractmethod
30
+ def build_query(filter_data):
31
+ pass
32
+
src/repositories/user.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.interfaces.auth import IPagination
2
+ from src.models.user import User
3
+ from src.repositories.base import ICrudRepository
4
+ from src.interfaces.user import ICreateUser, IFilterUser, IUpdateUser
5
+
6
+ from sqlalchemy import select, or_
7
+ from sqlalchemy.sql import Select
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+ from typing import Optional
10
+
11
+
12
+
13
+ class UserRepository(ICrudRepository):
14
+ async def store(self, db: AsyncSession, data: ICreateUser) -> User:
15
+ new_user = User(**data.model_dump())
16
+ db.add(new_user)
17
+ await db.commit()
18
+ await db.refresh(new_user)
19
+
20
+ return new_user
21
+
22
+ async def update(self, db: AsyncSession, user: User, data: IUpdateUser) -> User :
23
+ update_data = data.model_dump(exclude_unset=True)
24
+ for key, value in update_data.items():
25
+ if hasattr(user, key):
26
+ setattr(user, key, value)
27
+
28
+ await db.commit()
29
+ await db.refresh(user)
30
+ return user
31
+
32
+ async def delete(self, db: AsyncSession, user: User) :
33
+ await db.delete(user)
34
+ await db.commit()
35
+ return User(**user.model_dump())
36
+
37
+ async def find_by_pk(self, db: AsyncSession, user_id: int) -> Optional[User]:
38
+ return await db.get(User, user_id)
39
+
40
+ async def get_one(self, db: AsyncSession, filter_data: IFilterUser):
41
+ query = self.build_query(filter_data)
42
+ result = await db.execute(query)
43
+ return result.scalar_one_or_none()
44
+
45
+ async def get_many(self, db: AsyncSession, paging: IPagination, filter_data: IFilterUser):
46
+ query = self.build_query(filter_data)
47
+ query = query.limit(paging.limit).offset(paging.offset)
48
+ sort_by = getattr(User, paging.sort_by, None)
49
+ if sort_by is not None:
50
+ if paging.sort_order and paging.sort_order.lower() == 'desc':
51
+ query = query.order_by(sort_by.desc())
52
+ else:
53
+ query = query.order_by(sort_by.asc())
54
+ result = await db.execute(query)
55
+ return result.scalar().all()
56
+
57
+ @staticmethod
58
+ def build_query(filters: IFilterUser) -> Select:
59
+ query = select(User)
60
+ conditions = []
61
+ if filters.username_or_email:
62
+ conditions.append(
63
+ or_(
64
+ filters.username_or_email == User.email,
65
+ filters.username_or_email == User.username)
66
+ )
67
+ else:
68
+ if filters.username:
69
+ conditions.append(filters.username == User.username)
70
+ if filters.email:
71
+ conditions.append(filters.email == User.email)
72
+
73
+ return query.where(*conditions)
src/routers/public/__pycache__/auth.cpython-313.pyc ADDED
Binary file (1.85 kB). View file
 
src/routers/public/__pycache__/public.cpython-313.pyc ADDED
Binary file (578 Bytes). View file