@@ -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 import create_engine | ||||
from sqlalchemy.orm import sessionmaker | 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) | engine = create_engine(SQLALCHEMY_DATABASE_URL) | ||||
print(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 fastapi import FastAPI | ||||
from core.config import settings | from core.config import settings | ||||
from db.session import engine | 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) | Base.metadata.create_all(bind=engine) | ||||
include_routes(app) | |||||
return app | return app | ||||
app = startup() | |||||
app = startup() | |||||
# Testing stuff | |||||
@app.get("/") | @app.get("/") | ||||
def root(): | def root(): | ||||
return {"message": "Hello World!"} | return {"message": "Hello World!"} | ||||
@app.get("/user") | @app.get("/user") | ||||
def get_users(): | 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] | fastapi[all] | ||||
pydantic | |||||
sqlalchemy | sqlalchemy | ||||
psycopg2 | |||||
psycopg2 | |||||
alembic==1.12.0 | |||||
passlib |