| @@ -0,0 +1,116 @@ | |||
| # A generic, single database configuration. | |||
| [alembic] | |||
| # path to migration scripts | |||
| script_location = 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. | |||
| prepend_sys_path = . | |||
| # timezone to use when rendering the date within the migration file | |||
| # as well as the filename. | |||
| # If specified, requires the python-dateutil library that can be | |||
| # installed by adding `alembic[tz]` to the pip requirements | |||
| # string value is passed to dateutil.tz.gettz() | |||
| # 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 alembic/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 "version_path_separator" below. | |||
| # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions | |||
| # version path separator; As mentioned above, this is the character used to split | |||
| # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. | |||
| # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. | |||
| # Valid values for version_path_separator are: | |||
| # | |||
| # version_path_separator = : | |||
| # version_path_separator = ; | |||
| # version_path_separator = space | |||
| version_path_separator = os # Use os.pathsep. Default configuration used for new projects. | |||
| # 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 | |||
| 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 = --fix REVISION_SCRIPT_FILENAME | |||
| # Logging configuration | |||
| [loggers] | |||
| keys = root,sqlalchemy,alembic | |||
| [handlers] | |||
| keys = console | |||
| [formatters] | |||
| keys = generic | |||
| [logger_root] | |||
| level = WARN | |||
| handlers = console | |||
| qualname = | |||
| [logger_sqlalchemy] | |||
| level = WARN | |||
| 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 | |||
| @@ -0,0 +1 @@ | |||
| Generic single-database configuration. | |||
| @@ -0,0 +1,83 @@ | |||
| from logging.config import fileConfig | |||
| from sqlalchemy import engine_from_config | |||
| from sqlalchemy import pool | |||
| from alembic import context | |||
| from core.config import settings | |||
| from db.base import Base | |||
| # this is the Alembic Config object, which provides | |||
| # access to the values within the .ini file in use. | |||
| config = context.config | |||
| config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) | |||
| # 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 = 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() | |||
| @@ -0,0 +1,26 @@ | |||
| """${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: | |||
| ${upgrades if upgrades else "pass"} | |||
| def downgrade() -> None: | |||
| ${downgrades if downgrades else "pass"} | |||
| @@ -0,0 +1,60 @@ | |||
| """create User and Vehicle tables | |||
| Revision ID: 9a0214838ac8 | |||
| Revises: | |||
| Create Date: 2023-09-01 13:31:01.324861 | |||
| """ | |||
| from typing import Sequence, Union | |||
| from alembic import op | |||
| import sqlalchemy as sa | |||
| # revision identifiers, used by Alembic. | |||
| revision: str = '9a0214838ac8' | |||
| 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: | |||
| # ### commands auto generated by Alembic - please adjust! ### | |||
| op.create_table('users', | |||
| sa.Column('Id', sa.Integer(), nullable=False), | |||
| sa.Column('Name', sa.String(), nullable=False), | |||
| sa.Column('MiddleName', sa.String(), nullable=True), | |||
| sa.Column('LastName', sa.String(), nullable=False), | |||
| sa.Column('BirthDate', sa.DateTime(), nullable=False), | |||
| sa.Column('ContactNumber', sa.String(), nullable=False), | |||
| sa.Column('Email', sa.String(), nullable=False), | |||
| sa.Column('Role', sa.String(), nullable=False), | |||
| sa.Column('DrivingLicenseNumber', sa.String(), nullable=True), | |||
| sa.Column('HashedPassword', sa.String(), nullable=False), | |||
| sa.PrimaryKeyConstraint('Id') | |||
| ) | |||
| op.create_index(op.f('ix_users_Id'), 'users', ['Id'], unique=False) | |||
| op.create_table('vehicles', | |||
| sa.Column('Id', sa.Integer(), nullable=False), | |||
| sa.Column('Model', sa.String(), nullable=False), | |||
| sa.Column('Year', sa.Integer(), nullable=False), | |||
| sa.Column('LicensePlate', sa.String(), nullable=False), | |||
| sa.Column('Type', sa.String(), nullable=False), | |||
| sa.Column('AssignedDriverIds', sa.ARRAY(sa.Integer()), nullable=True), | |||
| sa.Column('CurrentLocation', sa.ARRAY(sa.String()), nullable=True), | |||
| sa.Column('Fuel', sa.Integer(), nullable=False), | |||
| sa.Column('Mileage', sa.Integer(), nullable=False), | |||
| sa.Column('MaintenanceNotes', sa.ARRAY(sa.String()), nullable=True), | |||
| sa.PrimaryKeyConstraint('Id') | |||
| ) | |||
| op.create_index(op.f('ix_vehicles_Id'), 'vehicles', ['Id'], unique=False) | |||
| # ### end Alembic commands ### | |||
| def downgrade() -> None: | |||
| # ### commands auto generated by Alembic - please adjust! ### | |||
| op.drop_index(op.f('ix_vehicles_Id'), table_name='vehicles') | |||
| op.drop_table('vehicles') | |||
| op.drop_index(op.f('ix_users_Id'), table_name='users') | |||
| op.drop_table('users') | |||
| # ### end Alembic commands ### | |||
| @@ -0,0 +1,7 @@ | |||
| # Base API router -- collecting all APIs here to not clutter main.py | |||
| from fastapi import APIRouter | |||
| from apis.v1 import route_user | |||
| api_router = APIRouter() | |||
| api_router.include_router(route_user.router, prefix="/user", tags=["users"]) | |||
| @@ -0,0 +1,17 @@ | |||
| # Routes for user. MAIN PART OF THE API | |||
| from fastapi import APIRouter, status | |||
| from sqlalchemy.orm import Session | |||
| from fastapi import Depends | |||
| from schemas.user import UserCreate, ShowUser | |||
| from db.session import get_db | |||
| from db.repository.user import create_new_user | |||
| router = APIRouter() | |||
| @router.post("/", response_model=ShowUser, status_code=status.HTTP_201_CREATED) | |||
| def create_user(user: UserCreate, db: Session = Depends(get_db)): | |||
| user = create_new_user(user=user, db=db) | |||
| return user | |||
| @@ -0,0 +1,14 @@ | |||
| # Hashing functions for passwords | |||
| from passlib.context import CryptContext | |||
| pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") | |||
| class Hasher: | |||
| @staticmethod | |||
| def verify_password(plain_password, hashed_password): | |||
| return pwd_context.verify(plain_password, hashed_password) | |||
| @staticmethod | |||
| def get_password_hash(plain_password): | |||
| return pwd_context.hash(plain_password) | |||
| @@ -0,0 +1,6 @@ | |||
| # Base class for all sqlalchemy models | |||
| from sqlalchemy.ext.declarative import declarative_base | |||
| Base = declarative_base() | |||
| from db.models.user import User | |||
| from db.models.vehicle import Vehicle | |||
| @@ -1,11 +0,0 @@ | |||
| from typing import Any | |||
| from sqlalchemy.ext.declarative import declared_attr | |||
| from sqlalchemy.orm import as_declarative | |||
| @as_declarative() | |||
| class Base: | |||
| id: Any | |||
| __name__: str | |||
| def __tablename__(cls) -> str: | |||
| return cls.__name__.lower() | |||
| @@ -0,0 +1,18 @@ | |||
| # PostgreSQL table model for users | |||
| from sqlalchemy import Column, Integer, String, DateTime, Boolean, URL | |||
| from sqlalchemy.orm import relationship | |||
| from db.base import Base | |||
| class User(Base): | |||
| __tablename__ = "users" | |||
| Id = Column(Integer, primary_key=True, index=True) | |||
| Name = Column(String, nullable=False) | |||
| MiddleName = Column(String, nullable=True) | |||
| LastName = Column(String, nullable=False) | |||
| BirthDate = Column(DateTime, nullable=False) | |||
| ContactNumber = Column(String, nullable=False) | |||
| Email = Column(String, nullable=False) | |||
| Role = Column(String, nullable=False) | |||
| DrivingLicenseNumber = Column(String, nullable=True) | |||
| HashedPassword = Column(String, nullable=False) | |||
| @@ -0,0 +1,27 @@ | |||
| # Postgres table model for vehicles | |||
| from sqlalchemy import ( | |||
| Column, | |||
| Integer, | |||
| String, | |||
| DateTime, | |||
| Boolean, | |||
| URL, | |||
| ARRAY, | |||
| ForeignKey, | |||
| ) | |||
| from sqlalchemy.orm import relationship | |||
| from db.base import Base | |||
| class Vehicle(Base): | |||
| __tablename__ = "vehicles" | |||
| Id = Column(Integer, primary_key=True, index=True) | |||
| Model = Column(String, nullable=False) | |||
| Year = Column(Integer, nullable=False) | |||
| LicensePlate = Column(String, nullable=False) | |||
| Type = Column(String, nullable=False) | |||
| AssignedDriverIds = Column(ARRAY(Integer), nullable=True) | |||
| CurrentLocation = Column(ARRAY(String), nullable=True) | |||
| Fuel = Column(Integer, nullable=False) | |||
| Mileage = Column(Integer, nullable=False) | |||
| MaintenanceNotes = Column(ARRAY(String), nullable=True) | |||
| @@ -0,0 +1,23 @@ | |||
| # Creating a new user in the database | |||
| from sqlalchemy.orm import Session | |||
| from schemas.user import UserCreate | |||
| from db.models.user import User | |||
| from core.hashing import Hasher | |||
| def create_new_user(user: UserCreate, db: Session): | |||
| user = User( | |||
| Email=user.email, | |||
| Name=user.name, | |||
| MiddleName=user.middlename, | |||
| LastName=user.lastname, | |||
| BirthDate=user.birthdate, | |||
| ContactNumber=user.phone, | |||
| Role=user.role, | |||
| HashedPassword=Hasher.get_password_hash(user.password), | |||
| ) | |||
| db.add(user) | |||
| db.commit() | |||
| db.refresh(user) | |||
| return user | |||
| @@ -1,11 +1,21 @@ | |||
| # Information about the database session is stored here | |||
| from sqlalchemy import create_engine | |||
| from sqlalchemy.orm import sessionmaker | |||
| from core.config import settings | |||
| from core.config import settings | |||
| SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL | |||
| SQLALCHEMY_DATABASE_URL = ( | |||
| settings.DATABASE_URL | |||
| ) # get the database url from core/config.py | |||
| engine = create_engine(SQLALCHEMY_DATABASE_URL) | |||
| print(SQLALCHEMY_DATABASE_URL) | |||
| SessionLocal = sessionmaker(autocommit=False,autoflush=False,bind=engine) | |||
| SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | |||
| def get_db(): # get the database session (if we change the database, we can change it here) | |||
| db = SessionLocal() | |||
| try: | |||
| yield db | |||
| finally: | |||
| db.close() | |||
| @@ -1,25 +1,30 @@ | |||
| from fastapi import FastAPI | |||
| from core.config import settings | |||
| from db.session import engine | |||
| from db.base_model import Base | |||
| from db.base import Base | |||
| from apis.base import api_router | |||
| def startup(): #start the project, and create the tables | |||
| app = FastAPI(title=settings.PROJECT_NAME,version=settings.PROJECT_VERSION) | |||
| def include_routes(app): # include all routes from our api/v1/ | |||
| app.include_router(api_router) | |||
| def startup(): # start the project, and create the tables | |||
| app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION) | |||
| Base.metadata.create_all(bind=engine) | |||
| include_routes(app) | |||
| return app | |||
| app = startup() | |||
| app = startup() | |||
| # Testing stuff | |||
| @app.get("/") | |||
| def root(): | |||
| return {"message": "Hello World!"} | |||
| @app.get("/user") | |||
| def get_users(): | |||
| return { | |||
| {"name": "almaz"}, | |||
| {"name": "madi"} | |||
| } | |||
| return {{"name": "almaz"}, {"name": "madi"}} | |||
| @@ -0,0 +1,30 @@ | |||
| # Purpose: User schema for pydantic (validation, inside-api usage) | |||
| from datetime import datetime | |||
| from pydantic import BaseModel, EmailStr, Field | |||
| class UserCreate(BaseModel): | |||
| email: EmailStr | |||
| password: str = Field(..., min_length=7, max_length=20) | |||
| name: str = Field(..., min_length=3, max_length=50) | |||
| middlename: str = Field(None, min_length=3, max_length=50) | |||
| lastname: str = Field(..., min_length=3, max_length=50) | |||
| phone: str = Field(..., min_length=12, max_length=12) | |||
| birthdate: datetime = Field(...) | |||
| email: EmailStr = Field(...) | |||
| role: str = Field(..., min_length=3, max_length=50) | |||
| class ShowUser(BaseModel): | |||
| Id: int | |||
| Name: str | |||
| MiddleName: str | |||
| LastName: str | |||
| ContactNumber: str | |||
| BirthDate: datetime | |||
| Email: EmailStr | |||
| Role: str | |||
| class Config: | |||
| orm_mode = True | |||
| @@ -1,3 +1,6 @@ | |||
| fastapi[all] | |||
| pydantic | |||
| sqlalchemy | |||
| psycopg2 | |||
| psycopg2 | |||
| alembic==1.12.0 | |||
| passlib | |||