diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb3dde7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.env +config.py +# Ignore Python cache files +__pycache__/ +*.pyc +*.pyo + +# Ignore init files if you don’t want them tracked +__init__.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..d4a6e52 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/postgres + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml new file mode 100644 index 0000000..24daca9 --- /dev/null +++ b/.idea/data_source_mapping.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/fastapi-demo-products.iml b/.idea/fastapi-demo-products.iml new file mode 100644 index 0000000..a6b95ba --- /dev/null +++ b/.idea/fastapi-demo-products.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..db14680 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a7351ee --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 0000000..5d8e01b --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/__pycache__/database.cpython-313.pyc b/__pycache__/database.cpython-313.pyc index 714e5d9..6727ab5 100644 Binary files a/__pycache__/database.cpython-313.pyc and b/__pycache__/database.cpython-313.pyc differ diff --git a/__pycache__/database_model.cpython-313.pyc b/__pycache__/database_model.cpython-313.pyc index 26c67e0..2367e17 100644 Binary files a/__pycache__/database_model.cpython-313.pyc and b/__pycache__/database_model.cpython-313.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc index 1dfe69f..95b7451 100644 Binary files a/__pycache__/main.cpython-313.pyc and b/__pycache__/main.cpython-313.pyc differ diff --git a/__pycache__/model.cpython-313.pyc b/__pycache__/model.cpython-313.pyc index 2dfe74a..3790d70 100644 Binary files a/__pycache__/model.cpython-313.pyc and b/__pycache__/model.cpython-313.pyc differ diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..6bfda86 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,147 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts. +# this is typically a path given in POSIX (e.g. forward slashes) +# format, relative to the token %(here)s which refers to the location of this +# ini file +script_location = %(here)s/alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. +prepend_sys_path = . + + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the tzdata library which can be installed by adding +# `alembic[tz]` to the pip requirements. +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to /versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "path_separator" +# below. +# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions + +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. +# +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: +# +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline +# +# Use os.pathsep. Default configuration used for new projects. +path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# database URL. This is consumed by the user-maintained env.py script only. +# other means of configuring database URLs may be customized within the env.py +# file. +sqlalchemy.url = postgresql://postgres:123@localhost:5432/inventory + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module +# hooks = ruff +# ruff.type = module +# ruff.module = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Alternatively, use the exec runner to execute a binary found on your PATH +# hooks = ruff +# ruff.type = exec +# ruff.executable = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Logging configuration. This is also consumed by the user-maintained +# env.py script only. +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..95c49bd --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,87 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context +from logging.config import fileConfig +from sqlalchemy import engine_from_config, pool +from alembic import context +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from app.database import Base +from app.models import user, company, product,todo +target_metadata = Base.metadata + + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..1101630 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/25d8f59a1dde_initial.py b/alembic/versions/25d8f59a1dde_initial.py new file mode 100644 index 0000000..0e3973d --- /dev/null +++ b/alembic/versions/25d8f59a1dde_initial.py @@ -0,0 +1,32 @@ +"""initial + +Revision ID: 25d8f59a1dde +Revises: +Create Date: 2025-10-13 14:14:29.332627 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '25d8f59a1dde' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/alembic/versions/34d17ef633a7_add_user_id_to_todos.py b/alembic/versions/34d17ef633a7_add_user_id_to_todos.py new file mode 100644 index 0000000..c36ea11 --- /dev/null +++ b/alembic/versions/34d17ef633a7_add_user_id_to_todos.py @@ -0,0 +1,34 @@ +"""add user_id to todos + +Revision ID: 34d17ef633a7 +Revises: bdd3c45ac166 +Create Date: 2025-10-21 18:16:28.695532 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '34d17ef633a7' +down_revision: Union[str, Sequence[str], None] = 'bdd3c45ac166' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('todos', sa.Column('user_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'todos', 'user', ['user_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'todos', type_='foreignkey') + op.drop_column('todos', 'user_id') + # ### end Alembic commands ### diff --git a/alembic/versions/998c60089b82_renamed_todos_to_todo.py b/alembic/versions/998c60089b82_renamed_todos_to_todo.py new file mode 100644 index 0000000..03df691 --- /dev/null +++ b/alembic/versions/998c60089b82_renamed_todos_to_todo.py @@ -0,0 +1,32 @@ +"""renamed todos to todo + +Revision ID: 998c60089b82 +Revises: b084aab31149 +Create Date: 2025-10-23 14:25:23.896922 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '998c60089b82' +down_revision: Union[str, Sequence[str], None] = 'b084aab31149' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.rename_table('todos', 'todo') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.rename_table('todo', 'todos') + # ### end Alembic commands ### diff --git a/alembic/versions/b084aab31149_changed_company_type_nullable_false_and_.py b/alembic/versions/b084aab31149_changed_company_type_nullable_false_and_.py new file mode 100644 index 0000000..80f95f3 --- /dev/null +++ b/alembic/versions/b084aab31149_changed_company_type_nullable_false_and_.py @@ -0,0 +1,62 @@ +"""Changed company_type nullable false and renamed todos to todo + +Revision ID: b084aab31149 +Revises: b688ad9a6411 +Create Date: 2025-10-22 16:53:29.419183 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'b084aab31149' +down_revision: Union[str, Sequence[str], None] = 'b688ad9a6411' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('todo', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=100), nullable=False), + sa.Column('description', sa.String(length=500), nullable=True), + sa.Column('published', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_todo_id'), 'todo', ['id'], unique=False) + op.drop_index(op.f('ix_todos_id'), table_name='todos') + op.drop_table('todos') + op.alter_column('company', 'company_type', + existing_type=sa.VARCHAR(), + nullable=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('company', 'company_type', + existing_type=sa.VARCHAR(), + nullable=True) + op.create_table('todos', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('title', sa.VARCHAR(length=100), autoincrement=False, nullable=False), + sa.Column('description', sa.VARCHAR(length=500), autoincrement=False, nullable=True), + sa.Column('published', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('todos_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('todos_pkey')) + ) + op.create_index(op.f('ix_todos_id'), 'todos', ['id'], unique=False) + op.drop_index(op.f('ix_todo_id'), table_name='todo') + op.drop_table('todo') + # ### end Alembic commands ### diff --git a/alembic/versions/b688ad9a6411_add_full_name_bio_profile_to_user.py b/alembic/versions/b688ad9a6411_add_full_name_bio_profile_to_user.py new file mode 100644 index 0000000..ea836ac --- /dev/null +++ b/alembic/versions/b688ad9a6411_add_full_name_bio_profile_to_user.py @@ -0,0 +1,36 @@ +"""Add full_name bio profile to user + +Revision ID: b688ad9a6411 +Revises: 34d17ef633a7 +Create Date: 2025-10-22 15:35:30.465282 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b688ad9a6411' +down_revision: Union[str, Sequence[str], None] = '34d17ef633a7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('full_name', sa.String(), nullable=True)) + op.add_column('user', sa.Column('bio', sa.String(), nullable=True)) + op.add_column('user', sa.Column('profile', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'profile') + op.drop_column('user', 'bio') + op.drop_column('user', 'full_name') + # ### end Alembic commands ### diff --git a/alembic/versions/b6ea24863180_renamed_type_to_company_type_in_company_.py b/alembic/versions/b6ea24863180_renamed_type_to_company_type_in_company_.py new file mode 100644 index 0000000..655051a --- /dev/null +++ b/alembic/versions/b6ea24863180_renamed_type_to_company_type_in_company_.py @@ -0,0 +1,34 @@ +"""renamed type to company_type in Company model + +Revision ID: b6ea24863180 +Revises: e1389d54a4b4 +Create Date: 2025-10-15 16:56:05.452033 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b6ea24863180' +down_revision: Union[str, Sequence[str], None] = 'e1389d54a4b4' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('company', sa.Column('company_type', sa.String(), nullable=True)) + op.drop_column('company', 'type') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('company', sa.Column('type', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_column('company', 'company_type') + # ### end Alembic commands ### diff --git a/alembic/versions/bdd3c45ac166_create_todo_table.py b/alembic/versions/bdd3c45ac166_create_todo_table.py new file mode 100644 index 0000000..c7efc09 --- /dev/null +++ b/alembic/versions/bdd3c45ac166_create_todo_table.py @@ -0,0 +1,41 @@ +"""create todo table + +Revision ID: bdd3c45ac166 +Revises: b6ea24863180 +Create Date: 2025-10-20 14:24:05.987653 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'bdd3c45ac166' +down_revision: Union[str, Sequence[str], None] = 'b6ea24863180' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('todos', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=100), nullable=False), + sa.Column('description', sa.String(length=500), nullable=True), + sa.Column('published', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_todos_id'), 'todos', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_todos_id'), table_name='todos') + op.drop_table('todos') + # ### end Alembic commands ### diff --git a/alembic/versions/e1389d54a4b4_add_type_column_to_company.py b/alembic/versions/e1389d54a4b4_add_type_column_to_company.py new file mode 100644 index 0000000..7475f33 --- /dev/null +++ b/alembic/versions/e1389d54a4b4_add_type_column_to_company.py @@ -0,0 +1,32 @@ +"""add type column to company + +Revision ID: e1389d54a4b4 +Revises: 25d8f59a1dde +Create Date: 2025-10-13 14:20:56.229293 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e1389d54a4b4' +down_revision: Union[str, Sequence[str], None] = '25d8f59a1dde' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('company', sa.Column('type', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('company', 'type') + # ### end Alembic commands ### diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..8e89768 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,56 @@ +from datetime import datetime, timedelta +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer +from jose import JWTError, jwt +from passlib.context import CryptContext +from sqlalchemy.orm import Session +from app.models.user import User +from app.database import get_db +from fastapi.security import HTTPAuthorizationCredentials + +from config import ACCESS_TOKEN_EXPIRE_DAYS, ALGORITHM, SECRET_KEY + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +bearer_scheme = HTTPBearer() + + +# ---------------- PASSWORD UTILS ---------------- # +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +# ---------------- TOKEN CREATION ---------------- # +def create_access_token(user_id: int): + expire = datetime.utcnow() + timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS) + to_encode = {"sub": str(user_id), "exp": expire} + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + +# ---------------- VERIFY CURRENT USER ---------------- # +def get_current_user( + token: HTTPAuthorizationCredentials = Depends(bearer_scheme), + db: Session = Depends(get_db) +): + credential_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + payload = jwt.decode(token.credentials, SECRET_KEY, + algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + raise credential_exception + except JWTError: + raise credential_exception + + user = db.query(User).filter(User.id == int(user_id)).first() + if user is None: + raise credential_exception + return user diff --git a/app/controllers/auth_controller.py b/app/controllers/auth_controller.py new file mode 100644 index 0000000..d8a85ba --- /dev/null +++ b/app/controllers/auth_controller.py @@ -0,0 +1,58 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.schemas.user import UserCreateRequest, UserResponse, UserLoginRequest, UserProfileResponse, UserUpdateRequest, \ + MessageResponse +from app.services.user_service import UserService +from app.database import get_db +from app.auth import create_access_token, verify_password, get_password_hash, get_current_user +from app.models.user import User +from config import config_reader + +router = APIRouter() + + +@router.post("/register", response_model=UserResponse) +def register(user: UserCreateRequest, db: Session = Depends(get_db)): + service = UserService(db) + hashed_pw= get_password_hash(user.password) + try: + return service.create_user(user.username, hashed_pw) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +@router.post("/token") +def login(user: UserLoginRequest, db: Session = Depends(get_db)): + + db_user = db.query(User).filter(User.username == user.username).first() + + if not db_user or not verify_password(user.password, db_user.password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") + + token = create_access_token(db_user.id) + return {"access_token": token} + +@router.get("/me", response_model=UserResponse) +def me( + db: Session = Depends(get_db), + current_user=Depends(get_current_user)): + service = UserService(db) + return service.get_user(current_user.id) + +@router.put("/update", response_model=UserProfileResponse) +def update( + user:UserUpdateRequest, + db: Session = Depends(get_db), + current_user=Depends(get_current_user), +): + service = UserService(db) + return service.updated_user(current_user.id, user.username,user.full_name, user.bio,user.profile) + + +@router.delete("/delete",response_model=MessageResponse) +def delete( db: Session = Depends(get_db), current_user=Depends(get_current_user)): + service = UserService(db) + deleted_user = service.delete_user(current_user.id) + return MessageResponse(message=config_reader.get_value("USER_DELETED_SUCCESSFULLY")) \ No newline at end of file diff --git a/app/controllers/company_controller.py b/app/controllers/company_controller.py new file mode 100644 index 0000000..635e0fa --- /dev/null +++ b/app/controllers/company_controller.py @@ -0,0 +1,48 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.schemas.company import CompanyCreateRequest, CompanyResponse, MessageResponse +from app.services.company_service import CompanyService +from app.database import get_db +from app.auth import get_current_user +from config import config_reader + +router = APIRouter() + + +@router.post("/", response_model=CompanyResponse) +def create_company( + company: CompanyCreateRequest, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.create_company(current_user.id, company.name,company.company_type, company.location) + + +@router.get("/me", response_model=CompanyResponse) +def get_my_company( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.get_company(current_user.id) + + +@router.put("/update-me",response_model=CompanyResponse) +def edit_my_company( + company: CompanyCreateRequest, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.edit_company(current_user.id, company.name, company.company_type, company.location) + + +@router.delete("/delete-me",response_model=MessageResponse) +def delete_my_company( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + service.delete_company(current_user.id) + return MessageResponse(message=config_reader.get_value("COMPANY_DELETE_MESSAGE")) diff --git a/app/controllers/product_controller.py b/app/controllers/product_controller.py new file mode 100644 index 0000000..b890db1 --- /dev/null +++ b/app/controllers/product_controller.py @@ -0,0 +1,62 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.schemas.product import ProductCreateRequest, ProductResponse, MessageResponse +from app.services.product_service import ProductService +from app.services.company_service import CompanyService +from app.database import get_db +from app.auth import get_current_user +from config import config_reader + +router = APIRouter() + + +@router.post("/", response_model=ProductResponse) +def create_product( + product: ProductCreateRequest, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + company_service = CompanyService(db) + product_service = ProductService(db) + company = company_service.get_company(current_user.id) + if not company: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="no company found") + + return product_service.create_product(company.id, product.name,product.price,product.description) + + +@router.get("/", response_model=list[ProductResponse]) +def list_product( + skip: int = 0, limit: int = 10, + db: Session = Depends(get_db), + +): + product_service = ProductService(db) + return product_service.list_products(skip,limit) + + +@router.get("/{product_id}", response_model=ProductResponse,dependencies=[Depends(get_current_user)]) +def get_product_by_id( + product_id: int, + db: Session = Depends(get_db), +): + product_service = ProductService(db) + return product_service.get_product(product_id) + +@router.put("/{product_id}", response_model=ProductResponse,dependencies=[Depends(get_current_user)]) +def update_product_by_id( + product_id: int, + product: ProductCreateRequest, + db: Session = Depends(get_db),): + product_service = ProductService(db) + return product_service.update_product(product_id,product.name,product.price,product.description) + +@router.delete("/{product_id}",dependencies=[Depends(get_current_user)],response_model=MessageResponse) +def delete_product_by_id( + product_id: int, + db: Session = Depends(get_db), + ): + product_service = ProductService(db) + product_service.delete_product(product_id) + return MessageResponse(message=config_reader.get_value("PRODUCT_DELETED_SUCCESSFULLY")) \ No newline at end of file diff --git a/app/controllers/profile_controller.py b/app/controllers/profile_controller.py new file mode 100644 index 0000000..aae2921 --- /dev/null +++ b/app/controllers/profile_controller.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.database import get_db +from app.auth import get_current_user +from app.services.profile_service import ProfileService + +from app.schemas.user import UserProfileResponse + +router = APIRouter() + + +@router.get("/",response_model=UserProfileResponse) +def get_profile( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = ProfileService(db) + return service.get_profile(current_user.id) diff --git a/app/controllers/todo_controller.py b/app/controllers/todo_controller.py new file mode 100644 index 0000000..2c0bd61 --- /dev/null +++ b/app/controllers/todo_controller.py @@ -0,0 +1,51 @@ +from typing import List +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.auth import get_current_user +from app.database import get_db +from app.schemas.todo import TodoResponse, TodoRequest, MessageResponse + +from app.services.todo_service import TodoService +from config import config_reader + +router = APIRouter() + +@router.get("/", response_model=List[TodoResponse]) +def get_all( + skip: int = 0, limit: int = 10, +db: Session = Depends(get_db), + +): + todo_service = TodoService(db) + return todo_service.get_all_todo(skip,limit) + + +@router.post("/", response_model=TodoResponse,dependencies=[Depends(get_current_user)]) +def create(todo : TodoRequest, + db: Session = Depends(get_db), + ): + + todo_service = TodoService(db) + return todo_service.create_todo(todo.title,todo.description,todo.published) + + + +@router.put("/{todo_id}",response_model=TodoResponse,dependencies=[Depends(get_current_user)]) +def update(todo_id:int, + todo : TodoRequest, + db: Session = Depends(get_db), + ): + + todo_service = TodoService(db) + return todo_service.update_todo(todo_id,todo.title,todo.description,todo.published) + + + +@router.delete("/{todo_id}",dependencies=[Depends(get_current_user)],response_model=MessageResponse) +def delete(todo_id:int, + db: Session = Depends(get_db), + ): + + todo_service = TodoService(db) + todo_service.delete_todo(todo_id) + return MessageResponse(message=config_reader.get_value("ITEM_DELETED_SUCCESSFULLY")) \ No newline at end of file diff --git a/database.py b/app/database.py similarity index 57% rename from database.py rename to app/database.py index dad789c..fbb7968 100644 --- a/database.py +++ b/app/database.py @@ -1,7 +1,20 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base + + db_url = "postgresql://postgres:123@localhost:5432/inventory" engine = create_engine(db_url) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..7a4cebd --- /dev/null +++ b/app/main.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from app.controllers import auth_controller, company_controller, product_controller, todo_controller, profile_controller +from app.middlewares.time_middleware import add_process_time_header + +version = "v1" +app = FastAPI(title="Company & Product API", + version=version + ) + +app.middleware("http")(add_process_time_header) + + +app.include_router(profile_controller.router, + prefix=f"/api/{version}/profiles", tags=["Profile"]) +app.include_router(auth_controller.router, + prefix=f"/api/{version}/auths", tags=["Auth"]) +app.include_router(company_controller.router, + prefix=f"/api/{version}/companies", tags=["Company"]) +app.include_router(product_controller.router, + prefix=f"/api/{version}/products", tags=["Product"]) +app.include_router(todo_controller.router, + prefix=f"/api/{version}/todos", tags=["Todo"]) + + diff --git a/app/middlewares/time_middleware.py b/app/middlewares/time_middleware.py new file mode 100644 index 0000000..9150207 --- /dev/null +++ b/app/middlewares/time_middleware.py @@ -0,0 +1,13 @@ +import time + +from fastapi import Request, APIRouter + +router = APIRouter() + + +async def add_process_time_header(request: Request, call_next): + start_time = time.perf_counter() + response = await call_next(request) + process_time = time.perf_counter() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response diff --git a/app/models/company.py b/app/models/company.py new file mode 100644 index 0000000..2362444 --- /dev/null +++ b/app/models/company.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship +from app.database import Base + + +class Company(Base): + __tablename__ = "company" + + id =Column(Integer, primary_key=True, index=True) + name= Column(String) + location = Column(String) + company_type = Column(String, nullable=False) + user_id = Column(Integer, ForeignKey("user.id")) + + + user= relationship("User", back_populates="company") + products= relationship("Product", back_populates="company") \ No newline at end of file diff --git a/app/models/product.py b/app/models/product.py new file mode 100644 index 0000000..b610eb1 --- /dev/null +++ b/app/models/product.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String, Float, ForeignKey +from sqlalchemy.orm import relationship +from app.database import Base + + +class Product(Base): + __tablename__ = "product" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String) + price = Column(Float) + description = Column(String,nullable=True) + company_id = Column(Integer, ForeignKey("company.id")) + + company = relationship("Company", back_populates="products") \ No newline at end of file diff --git a/app/models/todo.py b/app/models/todo.py new file mode 100644 index 0000000..1b8e1c6 --- /dev/null +++ b/app/models/todo.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime, func + + +from app.database import Base + +class Todo(Base): + __tablename__ = "todo" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(100), nullable=False) + description = Column(String(500), nullable=True) + published = Column(Boolean, default=True, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + + + + + diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..f2376c2 --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import relationship +from app.database import Base + + +class User(Base): + __tablename__ = "user" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True) + password = Column(String) + full_name = Column(String, nullable=True) + bio = Column(String, nullable=True) + profile = Column(String, nullable=True) + + company= relationship("Company",back_populates="user", uselist=False) + diff --git a/app/repositories/company_repository.py b/app/repositories/company_repository.py new file mode 100644 index 0000000..551efe1 --- /dev/null +++ b/app/repositories/company_repository.py @@ -0,0 +1,33 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session +from app.models.company import Company + + +class CompanyRepository: + def __init__(self, db: Session): + self.db = db + + def get_company_by_user_id(self, user_id: int): + return self.db.query(Company).filter(Company.user_id == user_id).first() + + def create(self, name:str, company_type:str, location:str): + created_company = Company(name=name, company_type=company_type, location=location) + self.db.add(created_company) + self.db.commit() + return created_company + + def update(self,user_id:int,name: str, company_type: str, location: str): + updated_company = self.db.query(Company).filter(Company.user_id == user_id).first() + if not updated_company: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="Company not found") + updated_company.name = name + updated_company.company_type = company_type + updated_company.location = location + + self.db.commit() + + return updated_company + + def delete(self, company:Company): + self.db.delete(company) + self.db.commit() diff --git a/app/repositories/product_repository.py b/app/repositories/product_repository.py new file mode 100644 index 0000000..c109568 --- /dev/null +++ b/app/repositories/product_repository.py @@ -0,0 +1,43 @@ +from fastapi import HTTPException,status +from sqlalchemy.orm import Session +from app.models.product import Product + +class ProductRepository: + def __init__(self, db: Session): + self.db = db + + def get_all(self, skip: int, limit: int): + return self.db.query(Product).offset(skip).limit(limit).all() + + def get_by_id(self, product_id: int): + return self.db.query(Product).filter(Product.id == product_id).first() + + def get_by_name_and_company(self, company_id: int, name: str): + return ( + self.db.query(Product) + .filter(Product.company_id == company_id, Product.name == name) + .first() + ) + + def create(self,company_id:int,name:str,price:float,description:str): + created_product = Product(company_id=company_id,name=name,price=price,description=description) + self.db.add(created_product) + self.db.commit() + self.db.refresh(created_product) + return created_product + + def update(self,product_id:int, name:str, price:float,description:str,): + product = self.db.query(Product).filter(Product.id == product_id).first() + + if not product: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="Product not found") + product.name = name + product.price = price + product.description = description + + self.db.commit() + return product + + def delete(self, product: Product): + self.db.delete(product) + self.db.commit() diff --git a/app/repositories/profile_repository.py b/app/repositories/profile_repository.py new file mode 100644 index 0000000..ad93db1 --- /dev/null +++ b/app/repositories/profile_repository.py @@ -0,0 +1,10 @@ +from sqlalchemy.orm import Session +from app.models.user import User + +class ProfileRepository: + def __init__(self, db: Session): + self.db = db + + + def get_by_user_id(self, user_id: int): + return self.db.query(User).filter(User.id == user_id).first() diff --git a/app/repositories/todo_repository.py b/app/repositories/todo_repository.py new file mode 100644 index 0000000..7024554 --- /dev/null +++ b/app/repositories/todo_repository.py @@ -0,0 +1,36 @@ +from sqlalchemy.orm import Session +from app.models.todo import Todo +from fastapi import HTTPException, status +class TodoRepository: + def __init__(self, db: Session): + self.db = db + + def list_todo(self, skip: int, limit: int): + return self.db.query(Todo).offset(skip).limit(limit).all() + + def get_todo_by_title(self, title: str): + return self.db.query(Todo).filter(Todo.title == title).first() + + def get_todo_by_id(self, todo_id: int): + return self.db.query(Todo).filter(Todo.id == todo_id).first() + + def create(self, title:str,description:str,published:bool): + created_todo = Todo(title=title,description=description,published=published) + self.db.add(created_todo) + self.db.commit() + return created_todo + + def update(self,todo_id:int,title:str,description:str,published:bool): + updated_todo = self.db.query(Todo).filter(Todo.id == todo_id).first() + if not updated_todo: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="todo item not found") + + updated_todo.title = title + updated_todo.description = description + updated_todo.published = published + self.db.commit() + return updated_todo + + def delete(self, todo: Todo): + self.db.delete(todo) + self.db.commit() diff --git a/app/repositories/user_repository.py b/app/repositories/user_repository.py new file mode 100644 index 0000000..65b02a1 --- /dev/null +++ b/app/repositories/user_repository.py @@ -0,0 +1,34 @@ +from sqlalchemy.orm import Session +from app.models.user import User + + +class UserRepository: + def __init__(self, db: Session): + self.db = db + + def get_by_id(self, user_id: int): + return self.db.query(User).filter(User.id == user_id).first() + + def get_by_username(self, username: str): + return self.db.query(User).filter(User.username == username).first() + + def create(self, username: str, hashed_pw: str): + new_user = User(username=username, password=hashed_pw) + self.db.add(new_user) + self.db.commit() + self.db.refresh(new_user) + return new_user + + def update(self, user_id:int,username: str, full_name: str,bio:str, profile:str,): + updated_user= self.db.query(User).filter(User.id == user_id).first() + updated_user.username = username + updated_user.full_name = full_name + updated_user.bio = bio + updated_user.profile = profile + + self.db.commit() + return updated_user + + def delete(self, user: User): + self.db.delete(user) + self.db.commit() diff --git a/app/schemas/company.py b/app/schemas/company.py new file mode 100644 index 0000000..f31cdb2 --- /dev/null +++ b/app/schemas/company.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel +from typing import List, Optional +from app.schemas.product import ProductResponse + + +class CompanyCreateRequest(BaseModel): + name: str + location: str + company_type: str + +class CompanyResponse(BaseModel): + id: int + name: str + location: str + company_type: str + products: List[ProductResponse] = [] + +class MessageResponse(BaseModel): + message: str + diff --git a/app/schemas/product.py b/app/schemas/product.py new file mode 100644 index 0000000..98be3d9 --- /dev/null +++ b/app/schemas/product.py @@ -0,0 +1,24 @@ +from typing import Optional +from pydantic import BaseModel,Field + + +class ProductCreateRequest(BaseModel): + name: str = Field(..., min_length=3, max_length=50, description="Product name") + price: float = Field(..., gt=0, description="Price must be greater than 0") + description: Optional[str] = Field(None, max_length=200) + + + +class ProductResponse(BaseModel): + id: int + name: str + price: float + description: Optional[str] = None + # + # class Config: + # from_attributes = True + + + +class MessageResponse(BaseModel): + message: str \ No newline at end of file diff --git a/app/schemas/todo.py b/app/schemas/todo.py new file mode 100644 index 0000000..cf69302 --- /dev/null +++ b/app/schemas/todo.py @@ -0,0 +1,32 @@ +from pydantic import BaseModel, Field, validator +from typing import Optional +from datetime import datetime + + +class TodoRequest(BaseModel): + title: str = Field(..., min_length=3, max_length=100) + description: Optional[str] = Field(None, max_length=500) + published: Optional[bool] = Field(default=True) + + @validator('title') + def no_empty_title(cls, v): + if not v.strip(): + raise ValueError('Title cannot be empty') + return v + + + +class TodoResponse(BaseModel): + id: int + title: str + description: Optional[str] = None + published: bool + created_at: datetime + + class Config: + from_attributes = True + + + +class MessageResponse(BaseModel): + message: str \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py new file mode 100644 index 0000000..ccad710 --- /dev/null +++ b/app/schemas/user.py @@ -0,0 +1,39 @@ +from pydantic import BaseModel,Field +from typing import Optional + +class UserCreateRequest(BaseModel): + username: str = Field(..., min_length=2, max_length=15, description="User's unique name") + password: str + full_name: Optional[str] = None + bio: Optional[str] = None + profile: Optional[str] = None + + +class UserUpdateRequest(BaseModel): + username: str = Field(..., min_length=2, max_length=15, description="User's unique name") + full_name: Optional[str] = None + bio: Optional[str] = None + profile: Optional[str] = None + +class UserResponse(BaseModel): + id: int + username: str + +class UserLoginRequest(BaseModel): + username: str + password: str + +class UserProfileResponse(BaseModel): + id: int + username: str + full_name: Optional[str] = None + bio: Optional[str] = None + profile: Optional[str] = None + + +class TokenResponse(BaseModel): + access_token: str + + +class MessageResponse(BaseModel): + message: str diff --git a/app/services/company_service.py b/app/services/company_service.py new file mode 100644 index 0000000..5a8b3f8 --- /dev/null +++ b/app/services/company_service.py @@ -0,0 +1,38 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session +from app.repositories.company_repository import CompanyRepository + + +class CompanyService: + def __init__(self, db: Session): + self.repo = CompanyRepository(db) + + def create_company(self, user_id: int, name: str, company_type: str, location: str): + existing = self.repo.get_company_by_user_id(user_id) + if existing: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Company already exists") + + created = self.repo.create(name, company_type, location) + return created + + def get_company(self, user_id: int): + company = self.repo.get_company_by_user_id(user_id) + if not company: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") + return company + + def edit_company(self, user_id: int, name: str, company_type: str, location: str): + company = self.repo.get_company_by_user_id(user_id) + if not company: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") + + updated = self.repo.update(user_id,name,company_type,location) + return updated + + def delete_company(self, user_id: int): + company = self.repo.get_company_by_user_id(user_id) + if not company: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") + + deleted = self.repo.delete(company) + return deleted diff --git a/app/services/product_service.py b/app/services/product_service.py new file mode 100644 index 0000000..24d24bb --- /dev/null +++ b/app/services/product_service.py @@ -0,0 +1,46 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session +from app.repositories.product_repository import ProductRepository + +class ProductService: + def __init__(self, db: Session): + self.repo = ProductRepository(db) + + def create_product(self, company_id: int, name: str, price: float, description: str): + existing_product = self.repo.get_by_name_and_company(company_id, name) + if existing_product: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="This product already exists for the company" + ) + + created_product = self.repo.create(company_id,name,price,description) + return created_product + + def list_products(self, skip: int, limit: int): + products = self.repo.get_all(skip, limit) + if not products: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No products found") + return products + + def get_product(self, product_id: int): + product = self.repo.get_by_id(product_id) + if not product: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") + return product + + def update_product(self, product_id: int, name: str, price: float, description: str): + product = self.repo.get_by_id(product_id) + if not product: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") + + updated = self.repo.update(product_id,name, price, description) + return updated + + def delete_product(self, product_id: int): + product = self.repo.get_by_id(product_id) + if not product: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") + + deleted = self.repo.delete(product) + return deleted diff --git a/app/services/profile_service.py b/app/services/profile_service.py new file mode 100644 index 0000000..e06e4e8 --- /dev/null +++ b/app/services/profile_service.py @@ -0,0 +1,15 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session + +from app.repositories.profile_repository import ProfileRepository + +class ProfileService: + def __init__(self, db: Session): + self.repo = ProfileRepository(db) + + + def get_profile(self, user_id: int): + profile = self.repo.get_by_user_id(user_id) + if not profile: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found") + return profile diff --git a/app/services/todo_service.py b/app/services/todo_service.py new file mode 100644 index 0000000..f8f3a11 --- /dev/null +++ b/app/services/todo_service.py @@ -0,0 +1,38 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session +from app.repositories.todo_repository import TodoRepository + +class TodoService: + def __init__(self, db: Session): + self.repo = TodoRepository(db) + + def get_all_todo(self,skip: int, limit: int): + todos = self.repo.list_todo(skip, limit) + if not todos: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No todos found") + + return todos + + def create_todo(self, title: str, description: str, published: bool): + existing = self.repo.get_todo_by_title(title) + if existing: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Todo already exists") + + create_todo = self.repo.create(title,description,published) + return create_todo + + def update_todo(self, todo_id: int, title: str, description: str, published: bool): + todo = self.repo.get_todo_by_id(todo_id) + if not todo: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") + + updated = self.repo.update(todo_id,title,description,published) + return updated + + def delete_todo(self, todo_id: int): + todo = self.repo.get_todo_by_id(todo_id) + if not todo: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") + + deleted = self.repo.delete(todo) + return deleted diff --git a/app/services/user_service.py b/app/services/user_service.py new file mode 100644 index 0000000..497aef4 --- /dev/null +++ b/app/services/user_service.py @@ -0,0 +1,46 @@ +from sqlalchemy.orm import Session +from fastapi import HTTPException, status +from app.repositories.user_repository import UserRepository + + +class UserService: + def __init__(self, db: Session): + self.repo = UserRepository(db) + + def create_user(self, username: str, hashed_pw: str): + existing_user = self.repo.get_by_username(username) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already exists" + ) + + create = self.repo.create(username, hashed_pw) + return create + + def get_user(self, user_id: int): + user = self.repo.get_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + return user + + def updated_user(self, user_id: int, username: str, full_name: str,bio:str, profile:str): + user = self.get_user(user_id) + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user not found") + updated = self.repo.update(user_id,username, full_name, bio,profile) + return updated + + def delete_user(self, user_id: int): + user = self.repo.get_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + deleted = self.repo.delete(user) + return deleted diff --git a/database_model.py b/database_model.py deleted file mode 100644 index ef72229..0000000 --- a/database_model.py +++ /dev/null @@ -1,14 +0,0 @@ -from sqlalchemy import Column, Integer, String, Float -from sqlalchemy.ext.declarative import declarative_base - -Base=declarative_base() - -class Product(Base): - - __tablename__ = "products" - - id = Column(Integer, primary_key=True, index=True) - name = Column(String) - description= Column(String) - price=Column(Float) - quantity=Column(Integer) \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index 412cd1c..0000000 --- a/main.py +++ /dev/null @@ -1,106 +0,0 @@ -from fastapi import Depends, FastAPI -from fastapi.middleware.cors import CORSMiddleware -from model import Product -from database import SessionLocal, engine -import database_model -from sqlalchemy.orm import Session - -app = FastAPI() - -app.add_middleware( - CORSMiddleware, - allow_origins=["http://localhost:3000"], - allow_methods=["*"], -) - -database_model.Base.metadata.create_all(bind=engine) - - -products = [ - Product(id=1, name="Laptop", description="A high-performance laptop", - price=999.99, quantity=10), - Product(id=2, name="Smartphone", - description="A latest model smartphone", price=699.99, quantity=25), - Product(id=3, name="Headphones", - description="Noise-cancelling headphones", price=199.99, quantity=15), - Product(id=4, name="Monitor", description="4K UHD Monitor", - price=299.99, quantity=8), - Product(id=5, name="Keyboard", description="Mechanical keyboard", - price=89.99, quantity=30), -] - - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - - -def init_db(): - db = SessionLocal() - count = db.query(database_model.Product).count() - - if count == 0: - for product in products: - db.add(database_model.Product(**product.model_dump())) - - db.commit() - - -init_db() - - -@app.get("/") -def greet(): - return "Hello, World!" - - -@app.get("/products") -def get_all_products(db: Session = Depends(get_db)): - db_products = db.query(database_model.Product).all() - return db_products - - -@app.get("/products/{id}") -def get_product_by_id(id: int, db: Session = Depends(get_db)): - db_product = db.query(database_model.Product).filter( - database_model.Product.id == id).first() - if db_product: - return db_product - return "product not found" - - -@app.post("/products") -def add_product(product: Product, db: Session = Depends(get_db)): - db.add(database_model.Product(**product.model_dump())) - db.commit() - return product - - -@app.put("/products/{id}") -def update_product(id: int, product: Product, db: Session = Depends(get_db)): - db_product = db.query(database_model.Product).filter( - database_model.Product.id == id).first() - if db_product: - db_product.name = product.name - db_product.description = product.description - db_product.price = product.price - db_product.quantity = product.quantity - db.commit() - return "product updated successfully" - else: - return "product not found" - - -@app.delete("/products/{id}") -def delete_product(id: int, db: Session = Depends(get_db)): - db_product = db.query(database_model.Product).filter( - database_model.Product.id == id).first() - if db_product: - db.delete(db_product) - db.commit() - return "product deleted successfully" - else: - return "product not found" diff --git a/model.py b/model.py deleted file mode 100644 index 23192ce..0000000 --- a/model.py +++ /dev/null @@ -1,8 +0,0 @@ -from pydantic import BaseModel - -class Product(BaseModel): - id:int - name:str - description:str - price:float - quantity:int \ No newline at end of file diff --git a/requirement.txt b/requirement.txt index 96c59e6..f081a46 100644 --- a/requirement.txt +++ b/requirement.txt @@ -1,9 +1,13 @@ annotated-types==0.7.0 anyio==4.11.0 +bcrypt==4.0.1 certifi==2025.10.5 +cffi==2.0.0 click==8.3.0 colorama==0.4.6 +cryptography==46.0.2 dnspython==2.8.0 +ecdsa==0.19.1 email-validator==2.3.0 fastapi==0.118.0 fastapi-cli==0.0.13 @@ -18,19 +22,25 @@ Jinja2==3.1.6 markdown-it-py==4.0.0 MarkupSafe==3.0.3 mdurl==0.1.2 +passlib==1.7.4 psycopg2==2.9.10 psycopg2-binary==2.9.10 +pyasn1==0.6.1 +pycparser==2.23 pydantic==2.11.10 pydantic_core==2.33.2 Pygments==2.19.2 python-dotenv==1.1.1 +python-jose==3.5.0 python-multipart==0.0.20 PyYAML==6.0.3 rich==14.1.0 rich-toolkit==0.15.1 rignore==0.7.0 +rsa==4.9.1 sentry-sdk==2.40.0 shellingham==1.5.4 +six==1.17.0 sniffio==1.3.1 SQLAlchemy==2.0.43 sqlmodel==0.0.25