From aa644913134f61de8e762b89ba7acafd11a9a156 Mon Sep 17 00:00:00 2001 From: LinMihigo Date: Mon, 2 Jun 2025 08:18:12 +0200 Subject: [PATCH] Add db & pydantic models + set up alembic --- .env | 9 ++ .gitignore | 1 + README.md | 7 + backend.py | 3 - backend/.dockerignore | 3 + backend/Dockerfile | 3 + backend/alembic.ini | 141 ++++++++++++++++++ backend/app/__init__.py | 0 backend/app/alembic/README | 1 + backend/app/alembic/env.py | 85 +++++++++++ backend/app/alembic/script.py.mako | 28 ++++ .../alembic/versions/5840d2b52dd8_rebuild.py | 90 +++++++++++ ...500_fix_client_id_in_credit_type_to_int.py | 60 ++++++++ backend/app/api/__init__.py | 0 backend/app/api/api.py | 7 + backend/app/auth.py | 0 backend/app/config.py | 41 +++++ backend/app/crud/__init__.py | 0 backend/app/crud/auth.py | 0 backend/app/crud/client.py | 15 ++ backend/app/crud/credit.py | 0 backend/app/crud/login.py | 0 backend/app/crud/payment.py | 0 backend/app/crud/product.py | 3 + backend/app/crud/supplier.py | 0 backend/app/db.py | 9 ++ backend/app/main.py | 20 +++ backend/app/models.py | 100 +++++++++++++ backend/requirements.txt | 0 db.sql => backend/scripts/db_setup.sql | 8 - backend/scripts/db_table_setup.sql | 6 + docker-compose.yml | 19 +++ mysql.sh | 5 - 33 files changed, 648 insertions(+), 16 deletions(-) create mode 100644 .env create mode 100644 .gitignore delete mode 100644 backend.py create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 backend/alembic.ini create mode 100644 backend/app/__init__.py create mode 100644 backend/app/alembic/README create mode 100644 backend/app/alembic/env.py create mode 100644 backend/app/alembic/script.py.mako create mode 100644 backend/app/alembic/versions/5840d2b52dd8_rebuild.py create mode 100644 backend/app/alembic/versions/bfb086d8d500_fix_client_id_in_credit_type_to_int.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/api.py create mode 100644 backend/app/auth.py create mode 100644 backend/app/config.py create mode 100644 backend/app/crud/__init__.py create mode 100644 backend/app/crud/auth.py create mode 100644 backend/app/crud/client.py create mode 100644 backend/app/crud/credit.py create mode 100644 backend/app/crud/login.py create mode 100644 backend/app/crud/payment.py create mode 100644 backend/app/crud/product.py create mode 100644 backend/app/crud/supplier.py create mode 100644 backend/app/db.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models.py create mode 100644 backend/requirements.txt rename db.sql => backend/scripts/db_setup.sql (64%) create mode 100644 backend/scripts/db_table_setup.sql create mode 100644 docker-compose.yml delete mode 100644 mysql.sh diff --git a/.env b/.env new file mode 100644 index 0000000..12e9435 --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +DATABASE_URL=mysql+mysqldb://admin:Avatarme1@localhost:3306/CMT + +# mysql +MYSQL_ROOT_PASSWORD=%40Avatarme1 +MYSQL_SERVER=localhost +MYSQL_PORT=3306 +MYSQL_DB=CMT +MYSQL_USER=admin +MYSQL_PASSWORD=Avatarme1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eeb8a6e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/__pycache__ diff --git a/README.md b/README.md index 0b9df4f..1dcda74 100644 --- a/README.md +++ b/README.md @@ -1 +1,8 @@ # CMT +### DB +```sql +-- db setup +cat db_setup.sql | mysql -u root -p +-- table setup +cat db_table_setup.sql | mysql -u admin -p CMT +``` diff --git a/backend.py b/backend.py deleted file mode 100644 index 0a46395..0000000 --- a/backend.py +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env python3 -"""This module contains the back end of the application -""" diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..2dfab68 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,3 @@ +# Python +__pycache__ +*.pyc diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..1dfef3a --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,3 @@ +FROM python:3.12-slim + +WORKDIR /app/ diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..b576264 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,141 @@ +# 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 = app/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 python>=3.9 or backports.zoneinfo library and tzdata library. +# Any required deps can 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 = driver://user:pass@localhost/dbname + + +[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 exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/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/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/alembic/README b/backend/app/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/backend/app/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/app/alembic/env.py b/backend/app/alembic/env.py new file mode 100644 index 0000000..94c6671 --- /dev/null +++ b/backend/app/alembic/env.py @@ -0,0 +1,85 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context +import os +from dotenv import load_dotenv +from sqlmodel import SQLModel +from app.models import Client, Supplier, Product, Payment, Credit + +load_dotenv() +# 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 +target_metadata = SQLModel.metadata +url = os.getenv("DATABASE_URL") + +# 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.", + url=url, + 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/backend/app/alembic/script.py.mako b/backend/app/alembic/script.py.mako new file mode 100644 index 0000000..480b130 --- /dev/null +++ b/backend/app/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, 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/backend/app/alembic/versions/5840d2b52dd8_rebuild.py b/backend/app/alembic/versions/5840d2b52dd8_rebuild.py new file mode 100644 index 0000000..a9f7cc7 --- /dev/null +++ b/backend/app/alembic/versions/5840d2b52dd8_rebuild.py @@ -0,0 +1,90 @@ +"""Rebuild + +Revision ID: 5840d2b52dd8 +Revises: +Create Date: 2025-06-01 14:27:25.657473 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = '5840d2b52dd8' +down_revision: Union[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! ### + op.create_table('client', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('tin_number', sa.Integer(), nullable=False), + sa.Column('names', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False), + sa.Column('phone_number', sqlmodel.sql.sqltypes.AutoString(length=10), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('tin_number') + ) + op.create_table('product', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('product_code', sqlmodel.sql.sqltypes.AutoString(length=10), nullable=False), + sa.Column('product_name', sqlmodel.sql.sqltypes.AutoString(length=20), nullable=False), + sa.Column('purchase_price', sa.Integer(), nullable=False), + sa.Column('date_modified', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('product_code'), + sa.UniqueConstraint('product_name') + ) + op.create_table('supplier', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('tin_number', sa.Integer(), nullable=False), + sa.Column('names', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False), + sa.Column('phone_number', sqlmodel.sql.sqltypes.AutoString(length=10), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('tin_number') + ) + op.create_table('credit', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('transcation_type', sa.Enum('BUY', 'SELL', name='tradetype'), nullable=False), + sa.Column('product_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('client_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('supplier_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('qty', sa.Integer(), nullable=False), + sa.Column('amount', sa.Integer(), nullable=False), + sa.Column('date', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['client.id'], ), + sa.ForeignKeyConstraint(['product_code'], ['product.product_code'], ), + sa.ForeignKeyConstraint(['supplier_id'], ['supplier.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('payment', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('payment_type', sa.Enum('BUY', 'SELL', name='tradetype'), nullable=False), + sa.Column('product_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('client_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('supplier_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('amount', sa.Integer(), nullable=False), + sa.Column('payment_method', sqlmodel.sql.sqltypes.AutoString(length=24), nullable=False), + sa.Column('date', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['client.id'], ), + sa.ForeignKeyConstraint(['product_code'], ['product.product_code'], ), + sa.ForeignKeyConstraint(['supplier_id'], ['supplier.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('payment') + op.drop_table('credit') + op.drop_table('supplier') + op.drop_table('product') + op.drop_table('client') + # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/bfb086d8d500_fix_client_id_in_credit_type_to_int.py b/backend/app/alembic/versions/bfb086d8d500_fix_client_id_in_credit_type_to_int.py new file mode 100644 index 0000000..b94ba87 --- /dev/null +++ b/backend/app/alembic/versions/bfb086d8d500_fix_client_id_in_credit_type_to_int.py @@ -0,0 +1,60 @@ +"""Fix client_id in Credit type to int + +Revision ID: bfb086d8d500 +Revises: 5840d2b52dd8 +Create Date: 2025-06-01 14:53:57.095181 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers, used by Alembic. +revision: str = 'bfb086d8d500' +down_revision: Union[str, None] = '5840d2b52dd8' +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('credit', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('transcation_type', sa.Enum('BUY', 'SELL', name='tradetype'), nullable=False), + sa.Column('product_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('client_id', sa.Integer(), nullable=False), + sa.Column('supplier_id', sa.Integer(), nullable=False), + sa.Column('qty', sa.Integer(), nullable=False), + sa.Column('amount', sa.Integer(), nullable=False), + sa.Column('date', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['client.id'], ), + sa.ForeignKeyConstraint(['product_code'], ['product.product_code'], ), + sa.ForeignKeyConstraint(['supplier_id'], ['supplier.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('payment', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('payment_type', sa.Enum('BUY', 'SELL', name='tradetype'), nullable=False), + sa.Column('product_code', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('client_id', sa.Integer(), nullable=False), + sa.Column('supplier_id', sa.Integer(), nullable=False), + sa.Column('amount', sa.Integer(), nullable=False), + sa.Column('payment_method', sqlmodel.sql.sqltypes.AutoString(length=24), nullable=False), + sa.Column('date', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['client.id'], ), + sa.ForeignKeyConstraint(['product_code'], ['product.product_code'], ), + sa.ForeignKeyConstraint(['supplier_id'], ['supplier.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('payment') + op.drop_table('credit') + # ### end Alembic commands ### diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/api.py b/backend/app/api/api.py new file mode 100644 index 0000000..0fb3589 --- /dev/null +++ b/backend/app/api/api.py @@ -0,0 +1,7 @@ +""" +API Home +""" +from fastapi import APIRouter + + +api_router = APIRouter() diff --git a/backend/app/auth.py b/backend/app/auth.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..4271663 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,41 @@ +import secrets +import warnings +from typing import Annotated, Any, Literal +from pydantic import ( + MySQLDsn +) +from pydantic_core import MultiHostUrl +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """ + """ + model_config = SettingsConfigDict( + # One level above ./backend + env_file='../.env', + env_ignore_empty=True, + extra='ignore' + ) + SECRET_KEY: str = secrets.token_urlsafe(32) + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days + + MYSQL_SERVER: str + MYSQL_PORT: int = 3306 + MYSQL_USER: str + MYSQL_PASSWORD: str = "" + MYSQL_DB: str = "" + + @computed_field # type: ignore[prop-decorator] + @property + def SQLALCHEMY_DATABASE_URI(self) -> MySQLDsn: + return MultiHostUrl.build( + scheme="mysql+mysqldb", + username=self.MYSQL_USER, + password=self.MYSQL_PASSWORD, + host=self.MYSQL_SERVER, + port=self.MYSQL_PORT, + path=self.MYSQL_DB + ) # type: ignore + +settings = Settings() # type: ignore diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/crud/auth.py b/backend/app/crud/auth.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/crud/client.py b/backend/app/crud/client.py new file mode 100644 index 0000000..db0d4e5 --- /dev/null +++ b/backend/app/crud/client.py @@ -0,0 +1,15 @@ +""" +The Client table +""" +from fastapi import APIRouter, HTTPException +from sqlmodel import func, select +from app.models import Client, Supplier, Product, Payment, Credit +from typing import Any + +router = APIRouter(prefix="/client", tags=["items"]) + + +@router.get("/", response_model=Client) +def read_clients( + session: SessionDep, +) diff --git a/backend/app/crud/credit.py b/backend/app/crud/credit.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/crud/login.py b/backend/app/crud/login.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/crud/payment.py b/backend/app/crud/payment.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/crud/product.py b/backend/app/crud/product.py new file mode 100644 index 0000000..e7c4a0d --- /dev/null +++ b/backend/app/crud/product.py @@ -0,0 +1,3 @@ +""" +TODO: when Credit.purchase_price is updated, update Product.purchase_price +""" diff --git a/backend/app/crud/supplier.py b/backend/app/crud/supplier.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/db.py b/backend/app/db.py new file mode 100644 index 0000000..9c74fa3 --- /dev/null +++ b/backend/app/db.py @@ -0,0 +1,9 @@ +from sqlmodel import Session, create_engine, select +from app.config import settings +from app.models import Client, Supplier + +engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI)) + + +def init_db(session: Session) -> None: + """""" diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..2e48503 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +"""Entry point for fastapi app + +NOTE: +- +""" +from app.config import settings +from typing import Union +from fastapi import FastAPI + +app = FastAPI( + title=settings.PROJECT_NAME, + openapi_url=f"{settings.API_V1_STR}/openapi.json" +) + +@app.get("/") +def read_root(): + """ + """ + return {"Hello": "World"} diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000..252d2c7 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,100 @@ +""" +This module contains Pydantic/Database Models that map database tables validate +and serialize api responses. + +If the logic is identical -> SQLModel is used to do both. +Otherwise pydantic - for api responses +And SQLAlchemy is used for db data validation. + +TODO: +Mapping & validation for: +- Clients, Suppliers, Products, payments + +Done: +* Table mappings +""" +from sqlmodel import SQLModel, Field, UniqueConstraint +from datetime import datetime +from sqlalchemy import Column, DateTime, func, Enum as SQLEnum +from enum import Enum +from typing import Optional + + +class TradeType(str, Enum): + BUY = "Buy" + SELL = "Sell" + + +class Client(SQLModel, table=True): + """Clients table mapping, api response validation and serialisation""" + id: Optional[int] = Field(default=None, primary_key=True) + tin_number: int = Field(nullable=False, unique=True) + names: str = Field(max_length=100, nullable=False) + phone_number: str = Field(max_length=10, nullable=False) + + +class Supplier(SQLModel, table=True): + """Supplier table mapping, api response validation and serialisation""" + id: Optional[int] = Field(default=None, primary_key=True) + tin_number: int = Field(nullable=False, unique=True) + names: str = Field(max_length=100, nullable=False) + phone_number: str = Field(max_length=10, nullable=False) + + +class Product(SQLModel, table=True): + """Products table mapping, api response validation and serialisation + + NOTE: purchase price should update every time a supplier credits us goods + and price has changed + """ + __table_args__ = (UniqueConstraint("product_code"),) + + id: Optional[int] = Field(nullable=False, primary_key=True) + product_code: str = Field(max_length=10, nullable=False) + product_name: str = Field(max_length=20, nullable=False, unique=True) + purchase_price: int = Field(nullable=False) + date_modified: datetime = Field( + sa_column=Column(DateTime, + server_default=func.now(), + server_onupdate=func.now()) + ) + + +class Payment(SQLModel, table=True): + """ + Payments table mapping, api response validation and serialisation + + Include both payments to suppliers and from clients + """ + id: Optional[int] = Field(default=None, primary_key=True) + payment_type: TradeType = Field( + sa_column=Column(SQLEnum(TradeType), nullable=False) + ) + product_code: str = Field(nullable=False, foreign_key="product.product_code") + client_id: Optional[int] = Field(nullable=False, foreign_key="client.id") + + supplier_id: Optional[int] = Field(nullable=False, foreign_key="supplier.id") + amount: int = Field(nullable=False) + payment_method: str = Field(max_length=24, nullable=False) + date: datetime = Field( + sa_column=Column(DateTime, server_default=func.now()) + ) + + +class Credit(SQLModel, table=True): + """Credit table mapping, api response validation and serialisation + + Include both credit from suppliers and to clients + """ + id: Optional[int] = Field(default=None, primary_key=True) + transcation_type: TradeType = Field( + sa_column=Column(SQLEnum(TradeType), nullable=False) + ) + product_code: str = Field(nullable=False, foreign_key="product.product_code") + client_id: Optional[int] = Field(nullable=False, foreign_key="client.id") + supplier_id: Optional[int] = Field(nullable=False, foreign_key="supplier.id") + qty: int = Field(nullable=False) + amount: int = Field(nullable=False) + date: datetime = Field( + sa_column=Column(DateTime, server_default=func.now()) + ) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/db.sql b/backend/scripts/db_setup.sql similarity index 64% rename from db.sql rename to backend/scripts/db_setup.sql index 2765315..1e991f2 100644 --- a/db.sql +++ b/backend/scripts/db_setup.sql @@ -5,11 +5,3 @@ IF NOT EXISTS 'admin'@'%' IDENTIFIED BY '@Avatarme1'; -- Grant rights to admin user GRANT ALL PRIVILEGES ON `CMT`.* TO 'admin'@'%'; FLUSH PRIVILEGES; - --- Create DB -CREATE DATABASE -IF NOT EXISTS CMT; - --- Create table -CREATE TABLE -IF NOT EXISTS clients; diff --git a/backend/scripts/db_table_setup.sql b/backend/scripts/db_table_setup.sql new file mode 100644 index 0000000..97a2932 --- /dev/null +++ b/backend/scripts/db_table_setup.sql @@ -0,0 +1,6 @@ + +-- Create DB +CREATE DATABASE +IF NOT EXISTS CMT; + +USE CMT; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1cc11ce --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + db: + image: mysql + restart: always + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h localhost -u${MYSQL_USER} -p${MYSQL_PASSWORD}"] + interval: 10s + timeout: 30s + retries: 5 + start_period: 30s + volumes: + - app-db-data:/var/lib/mysql + env_file: + - .env + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD?Variable not set} + - MYSQL_USER=${MYSQL_USER?Variable not set} + - MYSQL_PASSWORD=${MYSQL_PASSWORD?Variable not set} + - MYSQL_DATABASE=${MYSQL_DATABASE?Variable not set} diff --git a/mysql.sh b/mysql.sh deleted file mode 100644 index b74e941..0000000 --- a/mysql.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -# Setting up the mysql -sudo apt update -y && sudo apt upgrade -y -sudo apt install mysql-server