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