├── migrations ├── README ├── versions │ ├── __pycache__ │ │ ├── bc95443719a6_init.cpython-39.pyc │ │ └── f47a0abc7f1d_add_seller_to_gem_model.cpython-39.pyc │ ├── f47a0abc7f1d_add_seller_to_gem_model.py │ └── bc95443719a6_init.py ├── script.py.mako └── env.py ├── .gitignore ├── database.db ├── requirements.txt ├── README.md ├── deployment_configurations ├── README.txt ├── nginx.txt ├── fastapi.service.txt └── gunicorn.conf.txt ├── db └── db.py ├── repos ├── user_repos.py └── gem_repository.py ├── main.py ├── models ├── user_models.py └── gem_models.py ├── endpoints ├── user_endpoints.py └── gem_endpoints.py ├── populate.py ├── auth └── auth.py └── alembic.ini /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .idea 3 | __pycache__ 4 | */__pycache__ 5 | -------------------------------------------------------------------------------- /database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creepykedi/Fastapi-jewels-tutorial/HEAD/database.db -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | sqlmodel 3 | uvicorn 4 | alembic 5 | pyJWT 6 | bcrypt 7 | passlib 8 | pydantic[email] 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fastapi-jewels-tutorial 2 | Repo for my FasAPI youtube tutorial 3 | 4 | Full video can be found here: 5 | https://youtu.be/8EoVjYw3V2g 6 | -------------------------------------------------------------------------------- /deployment_configurations/README.txt: -------------------------------------------------------------------------------- 1 | If you want files to work just delete the .txt extension an the end of each. Otherwise, you can read or copy the data from them. -------------------------------------------------------------------------------- /migrations/versions/__pycache__/bc95443719a6_init.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creepykedi/Fastapi-jewels-tutorial/HEAD/migrations/versions/__pycache__/bc95443719a6_init.cpython-39.pyc -------------------------------------------------------------------------------- /db/db.py: -------------------------------------------------------------------------------- 1 | from sqlmodel import create_engine 2 | from sqlmodel import Session 3 | eng = 'database.db' 4 | 5 | sqlite_url = f'sqlite:///{eng}' 6 | engine = create_engine(sqlite_url, echo=True) 7 | session = Session(bind=engine) 8 | -------------------------------------------------------------------------------- /migrations/versions/__pycache__/f47a0abc7f1d_add_seller_to_gem_model.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creepykedi/Fastapi-jewels-tutorial/HEAD/migrations/versions/__pycache__/f47a0abc7f1d_add_seller_to_gem_model.cpython-39.pyc -------------------------------------------------------------------------------- /deployment_configurations/nginx.txt: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name www.yourwebsite.com yourwebsite.com; 4 | 5 | location / { 6 | proxy_pass http://unix:/Fastapi-jewels-tutorial/gunicorn.sock; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /deployment_configurations/fastapi.service.txt: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Gunicorn Daemon for FastAPI Application 3 | After=network.target 4 | 5 | [Service] 6 | User=root 7 | Group=root 8 | WorkingDirectory=/root/Fastapi-jewels-tutorial 9 | ExecStart=/root/Fastapi-jewels-tutorial/venv/bin/python3 /root/Fastapi-jewels-tutorial/venv/bin/gunicorn -c gunicorn_conf.py main:app 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /deployment_configurations/gunicorn.conf.txt: -------------------------------------------------------------------------------- 1 | from multiprocessing import cpu_count 2 | 3 | # Socket Path 4 | bind = 'unix:/root/Fastapi-jewels-tutorial/gunicorn.sock' 5 | 6 | # Worker Options 7 | workers = cpu_count() + 1 8 | worker_class = 'uvicorn.workers.UvicornWorker' 9 | 10 | # Logging Options 11 | loglevel = 'debug' 12 | accesslog = '/root/Fastapi-jewels-tutorial/access_log' 13 | errorlog = '/root/Fastapi-jewels-tutorial/error_log' 14 | -------------------------------------------------------------------------------- /repos/user_repos.py: -------------------------------------------------------------------------------- 1 | from sqlmodel import Session, select 2 | 3 | from db.db import engine 4 | from models.user_models import User 5 | 6 | 7 | def select_all_users(): 8 | with Session(engine) as session: 9 | statement = select(User) 10 | res = session.exec(statement).all() 11 | return res 12 | 13 | 14 | def find_user(name): 15 | with Session(engine) as session: 16 | statement = select(User).where(User.username == name) 17 | return session.exec(statement).first() 18 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | import uvicorn 3 | from endpoints.gem_endpoints import gem_router 4 | from endpoints.user_endpoints import user_router 5 | from models.gem_models import * 6 | 7 | 8 | app = FastAPI() 9 | 10 | app.include_router(gem_router) 11 | app.include_router(user_router) 12 | 13 | 14 | 15 | # def create_db_and_tables(): 16 | # SQLModel.metadata.create_all(engine) 17 | 18 | 19 | if __name__ == '__main__': 20 | uvicorn.run('main:app', host="localhost", port=8000, reload=True) 21 | #create_db_and_tables() 22 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlmodel 11 | ${imports if imports else ""} 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = ${repr(up_revision)} 15 | down_revision = ${repr(down_revision)} 16 | branch_labels = ${repr(branch_labels)} 17 | depends_on = ${repr(depends_on)} 18 | 19 | 20 | def upgrade(): 21 | ${upgrades if upgrades else "pass"} 22 | 23 | 24 | def downgrade(): 25 | ${downgrades if downgrades else "pass"} 26 | -------------------------------------------------------------------------------- /repos/gem_repository.py: -------------------------------------------------------------------------------- 1 | from db.db import engine 2 | from models.gem_models import Gem, GemProperties 3 | from sqlmodel import Session, select, or_ 4 | 5 | 6 | def select_all_gems(): 7 | with Session(engine) as session: 8 | statement = select(Gem, GemProperties).join(GemProperties) 9 | # statement = statement.where(Gem.id > 0).where(Gem.id < 2) 10 | #statement = statement.where(or_(Gem.id>1, Gem.price!=2000)) 11 | result = session.exec(statement) 12 | res = [] 13 | for gem, props in result: 14 | res.append({'gem': gem, 'props': props}) 15 | return res 16 | 17 | 18 | def select_gem(id): 19 | with Session(engine) as session: 20 | statement = select(Gem, GemProperties).join(GemProperties) 21 | statement = statement.where(Gem.id==id) 22 | result = session.exec(statement) 23 | return result.first() 24 | 25 | 26 | #select_gems() -------------------------------------------------------------------------------- /migrations/versions/f47a0abc7f1d_add_seller_to_gem_model.py: -------------------------------------------------------------------------------- 1 | """add seller to gem model 2 | 3 | Revision ID: f47a0abc7f1d 4 | Revises: bc95443719a6 5 | Create Date: 2022-04-23 23:52:29.159924 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlmodel 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'f47a0abc7f1d' 15 | down_revision = 'bc95443719a6' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.add_column('gem', sa.Column('seller_id', sa.Integer(), nullable=True)) 23 | op.create_foreign_key(None, 'gem', 'user', ['seller_id'], ['id']) 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.drop_constraint(None, 'gem', type_='foreignkey') 30 | op.drop_column('gem', 'seller_id') 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /models/user_models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional 3 | 4 | from pydantic import validator, EmailStr 5 | from sqlmodel import SQLModel, Field, Relationship 6 | 7 | 8 | class User(SQLModel, table=True): 9 | id: Optional[int] = Field(primary_key=True) 10 | username: str = Field(index=True) 11 | password: str = Field(max_length=256, min_length=6) 12 | email: EmailStr 13 | created_at: datetime.datetime = datetime.datetime.now() 14 | is_seller: bool = False 15 | 16 | 17 | class UserInput(SQLModel): 18 | username: str 19 | password: str = Field(max_length=256, min_length=6) 20 | password2: str 21 | email: EmailStr 22 | is_seller: bool = False 23 | 24 | @validator('password2') 25 | def password_match(cls, v, values, **kwargs): 26 | if 'password' in values and v != values['password']: 27 | raise ValueError('passwords don\'t match') 28 | return v 29 | 30 | 31 | class UserLogin(SQLModel): 32 | username: str 33 | password: str 34 | -------------------------------------------------------------------------------- /migrations/versions/bc95443719a6_init.py: -------------------------------------------------------------------------------- 1 | """init 2 | 3 | Revision ID: bc95443719a6 4 | Revises: 5 | Create Date: 2022-04-23 22:52:02.046623 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlmodel 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'bc95443719a6' 15 | down_revision = None 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | op.create_table('user', 23 | sa.Column('id', sa.Integer(), nullable=True), 24 | sa.Column('username', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 25 | sa.Column('password', sqlmodel.sql.sqltypes.AutoString(length=256), nullable=False), 26 | sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 27 | sa.Column('created_at', sa.DateTime(), nullable=True), 28 | sa.Column('is_seller', sa.Boolean(), nullable=True), 29 | sa.PrimaryKeyConstraint('id') 30 | ) 31 | op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=False) 32 | # ### end Alembic commands ### 33 | 34 | 35 | def downgrade(): 36 | # ### commands auto generated by Alembic - please adjust! ### 37 | op.drop_index(op.f('ix_user_username'), table_name='user') 38 | op.drop_table('user') 39 | # ### end Alembic commands ### 40 | -------------------------------------------------------------------------------- /endpoints/user_endpoints.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException, Security, security, Depends 2 | from fastapi.security import HTTPAuthorizationCredentials 3 | from starlette.responses import JSONResponse 4 | from starlette.status import HTTP_201_CREATED,HTTP_404_NOT_FOUND 5 | 6 | from auth.auth import AuthHandler 7 | from db.db import session 8 | from models.user_models import UserInput, User, UserLogin 9 | from repos.user_repos import select_all_users, find_user 10 | 11 | user_router = APIRouter() 12 | auth_handler = AuthHandler() 13 | 14 | 15 | @user_router.post('/registration', status_code=201, tags=['users'], 16 | description='Register new user') 17 | def register(user: UserInput): 18 | users = select_all_users() 19 | if any(x.username == user.username for x in users): 20 | raise HTTPException(status_code=400, detail='Username is taken') 21 | hashed_pwd = auth_handler.get_password_hash(user.password) 22 | u = User(username=user.username, password=hashed_pwd, email=user.email, 23 | is_seller=user.is_seller) 24 | session.add(u) 25 | session.commit() 26 | return JSONResponse(status_code=HTTP_201_CREATED) 27 | 28 | 29 | @user_router.post('/login', tags=['users']) 30 | def login(user: UserLogin): 31 | user_found = find_user(user.username) 32 | if not user_found: 33 | raise HTTPException(status_code=401, detail='Invalid username and/or password') 34 | verified = auth_handler.verify_password(user.password, user_found.password) 35 | if not verified: 36 | raise HTTPException(status_code=401, detail='Invalid username and/or password') 37 | token = auth_handler.encode_token(user_found.username) 38 | return {'token': token} 39 | 40 | 41 | @user_router.get('/users/me', tags=['users']) 42 | def get_current_user(user: User = Depends(auth_handler.get_current_user)): 43 | return user 44 | -------------------------------------------------------------------------------- /models/gem_models.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from sqlmodel import SQLModel, Field, Relationship 3 | from enum import Enum as Enum_, IntEnum 4 | 5 | from models.user_models import User 6 | 7 | 8 | class Enum(Enum_): 9 | @classmethod 10 | def list(cls): 11 | return list(map(lambda c: c.value, cls)) 12 | 13 | 14 | class GemTypes(str, Enum): 15 | DIAMOND = 'DIAMOND' 16 | RUBY = 'RUBY' 17 | EMERALD = 'EMERALD' 18 | 19 | 20 | class GemClarity(IntEnum): 21 | SI = 1 22 | VS = 2 23 | VVS = 3 24 | FL = 4 25 | 26 | 27 | class GemColor(str, Enum): 28 | D = 'D' 29 | E = 'E' 30 | G = 'G' 31 | F = 'F' 32 | H = 'H' 33 | I = 'I' 34 | 35 | 36 | class GemProperties(SQLModel, table=True): 37 | id: Optional[int] = Field(primary_key=True) 38 | size: float = 1 39 | clarity: Optional[GemClarity] = None 40 | color: Optional[GemColor] = None 41 | gem: Optional['Gem'] = Relationship(back_populates='gem_properties') 42 | 43 | 44 | class Gem(SQLModel, table=True): 45 | id: Optional[int] = Field(primary_key=True) 46 | price: float 47 | available: bool = True 48 | gem_type: GemTypes = GemTypes.DIAMOND 49 | gem_properties_id: Optional[int] = Field(default=None, foreign_key='gemproperties.id') 50 | gem_properties: Optional[GemProperties] = Relationship(back_populates='gem') 51 | seller_id: Optional[int] = Field(default=None, foreign_key='user.id') 52 | seller: Optional[User] = Relationship() 53 | 54 | 55 | class GemPatch(SQLModel): 56 | id: Optional[int] = Field(primary_key=True) 57 | price: Optional[float] = 1000 58 | available: Optional[bool] = True 59 | gem_type: Optional[GemTypes] = GemTypes.DIAMOND 60 | 61 | gem_properties_id: Optional[int] = Field(default=None, foreign_key='gemproperties.id') 62 | gem_properties: Optional[GemProperties] = Relationship(back_populates='gem') -------------------------------------------------------------------------------- /populate.py: -------------------------------------------------------------------------------- 1 | import random 2 | from sqlmodel import Session, select 3 | from db.db import engine 4 | from models.gem_models import Gem, GemProperties, GemTypes, GemColor 5 | 6 | color_multiplier = { 7 | 'D': 1.8, 8 | 'E': 1.6, 9 | 'G': 1.4, 10 | 'F': 1.2, 11 | 'H': 1, 12 | 'I': 0.8 13 | } 14 | 15 | 16 | def calculate_gem_price(gem, gem_pr): 17 | price = 1000 18 | if gem.gem_type == 'Ruby': 19 | price = 400 20 | elif gem.gem_type == 'Emerald': 21 | price = 650 22 | 23 | if gem_pr.clarity == 1: 24 | price *= 0.75 25 | elif gem_pr.clarity == 3: 26 | price *= 1.25 27 | elif gem_pr.clarity == 4: 28 | price *= 1.5 29 | 30 | price = price * (gem_pr.size**3) 31 | 32 | if gem.gem_type == 'Diamond': 33 | multiplier = color_multiplier[gem_pr.color] 34 | price *= multiplier 35 | 36 | return price 37 | 38 | 39 | def create_gem_props(): 40 | size = random.randint(3, 70)/10 41 | color = random.choice(GemColor.list()) 42 | clarity = random.randint(1, 4) 43 | 44 | gemp_p = GemProperties(size=size, clarity=clarity, 45 | color=color) 46 | return gemp_p 47 | 48 | 49 | def create_gem(gem_p): 50 | type = random.choice(GemTypes.list()) 51 | gem = Gem(price=1000, gem_properties_id=gem_p.id, gem_type=type) 52 | price = calculate_gem_price(gem, gem_p) 53 | price = round(price, 2) 54 | gem.price = price 55 | return gem 56 | 57 | 58 | def create_gems_db(): 59 | #gem_p = create_gem_props() 60 | gem_ps = [create_gem_props() for x in range(100)] 61 | print(gem_ps) 62 | with Session(engine) as session: 63 | session.add_all(gem_ps) 64 | session.commit() 65 | gems = [create_gem(gem_ps[x]) for x in range(100)] 66 | # g = create_gem(gem_p.id) 67 | session.add_all(gems) 68 | session.commit() 69 | 70 | 71 | #create_gems_db() 72 | -------------------------------------------------------------------------------- /auth/auth.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from fastapi import Security, HTTPException 4 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 5 | from passlib.context import CryptContext 6 | import jwt 7 | from starlette import status 8 | 9 | from repos.user_repos import find_user 10 | 11 | 12 | class AuthHandler: 13 | security = HTTPBearer() 14 | pwd_context = CryptContext(schemes=['bcrypt']) 15 | secret = 'supersecret' 16 | 17 | def get_password_hash(self, password): 18 | return self.pwd_context.hash(password) 19 | 20 | def verify_password(self, pwd, hashed_pwd): 21 | return self.pwd_context.verify(pwd, hashed_pwd) 22 | 23 | def encode_token(self, user_id): 24 | payload = { 25 | 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=8), 26 | 'iat': datetime.datetime.utcnow(), 27 | 'sub': user_id 28 | } 29 | return jwt.encode(payload, self.secret, algorithm='HS256') 30 | 31 | def decode_token(self, token): 32 | try: 33 | payload = jwt.decode(token, self.secret, algorithms=['HS256']) 34 | return payload['sub'] 35 | except jwt.ExpiredSignatureError: 36 | raise HTTPException(status_code=401, detail='Expired signature') 37 | except jwt.InvalidTokenError: 38 | raise HTTPException(status_code=401, detail='Invalid token') 39 | 40 | def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)): 41 | return self.decode_token(auth.credentials) 42 | 43 | def get_current_user(self, auth: HTTPAuthorizationCredentials = Security(security)): 44 | credentials_exception = HTTPException( 45 | status_code=status.HTTP_401_UNAUTHORIZED, 46 | detail='Could not validate credentials' 47 | ) 48 | username = self.decode_token(auth.credentials) 49 | if username is None: 50 | raise credentials_exception 51 | user = find_user(username) 52 | if user is None: 53 | raise credentials_exception 54 | return user -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | from sqlmodel import SQLModel 6 | from models.gem_models import * 7 | from models.user_models import * 8 | from alembic import context 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | if config.config_file_name is not None: 17 | fileConfig(config.config_file_name) 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | target_metadata = SQLModel.metadata 24 | 25 | # other values from the config, defined by the needs of env.py, 26 | # can be acquired: 27 | # my_important_option = config.get_main_option("my_important_option") 28 | # ... etc. 29 | 30 | 31 | def run_migrations_offline(): 32 | """Run migrations in 'offline' mode. 33 | 34 | This configures the context with just a URL 35 | and not an Engine, though an Engine is acceptable 36 | here as well. By skipping the Engine creation 37 | we don't even need a DBAPI to be available. 38 | 39 | Calls to context.execute() here emit the given string to the 40 | script output. 41 | 42 | """ 43 | url = config.get_main_option("sqlalchemy.url") 44 | context.configure( 45 | url=url, 46 | target_metadata=target_metadata, 47 | literal_binds=True, 48 | dialect_opts={"paramstyle": "named"}, render_as_batch=True 49 | ) 50 | 51 | with context.begin_transaction(): 52 | context.run_migrations() 53 | 54 | 55 | def run_migrations_online(): 56 | """Run migrations in 'online' mode. 57 | 58 | In this scenario we need to create an Engine 59 | and associate a connection with the context. 60 | 61 | """ 62 | connectable = engine_from_config( 63 | config.get_section(config.config_ini_section), 64 | prefix="sqlalchemy.", 65 | poolclass=pool.NullPool, 66 | ) 67 | 68 | with connectable.connect() as connection: 69 | context.configure( 70 | connection=connection, target_metadata=target_metadata, render_as_batch=True 71 | ) 72 | 73 | with context.begin_transaction(): 74 | context.run_migrations() 75 | 76 | 77 | if context.is_offline_mode(): 78 | run_migrations_offline() 79 | else: 80 | run_migrations_online() 81 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # sys.path path, will be prepended to sys.path if present. 11 | # defaults to the current working directory. 12 | prepend_sys_path = . 13 | 14 | # timezone to use when rendering the date within the migration file 15 | # as well as the filename. 16 | # If specified, requires the python-dateutil library that can be 17 | # installed by adding `alembic[tz]` to the pip requirements 18 | # string value is passed to dateutil.tz.gettz() 19 | # leave blank for localtime 20 | # timezone = 21 | 22 | # max length of characters to apply to the 23 | # "slug" field 24 | # truncate_slug_length = 40 25 | 26 | # set to 'true' to run the environment during 27 | # the 'revision' command, regardless of autogenerate 28 | # revision_environment = false 29 | 30 | # set to 'true' to allow .pyc and .pyo files without 31 | # a source .py file to be detected as revisions in the 32 | # versions/ directory 33 | # sourceless = false 34 | 35 | # version location specification; This defaults 36 | # to migrations/versions. When using multiple version 37 | # directories, initial revisions must be specified with --version-path. 38 | # The path separator used here should be the separator specified by "version_path_separator" below. 39 | # version_locations = %(here)s/bar:%(here)s/bat:migrations/versions 40 | 41 | # version path separator; As mentioned above, this is the character used to split 42 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 43 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 44 | # Valid values for version_path_separator are: 45 | # 46 | # version_path_separator = : 47 | # version_path_separator = ; 48 | # version_path_separator = space 49 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 50 | 51 | # the output encoding used when revision files 52 | # are written from script.py.mako 53 | # output_encoding = utf-8 54 | 55 | sqlalchemy.url = sqlite:///database.db 56 | 57 | 58 | [post_write_hooks] 59 | # post_write_hooks defines scripts or Python functions that are run 60 | # on newly generated revision scripts. See the documentation for further 61 | # detail and examples 62 | 63 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 64 | # hooks = black 65 | # black.type = console_scripts 66 | # black.entrypoint = black 67 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 68 | 69 | # Logging configuration 70 | [loggers] 71 | keys = root,sqlalchemy,alembic 72 | 73 | [handlers] 74 | keys = console 75 | 76 | [formatters] 77 | keys = generic 78 | 79 | [logger_root] 80 | level = WARN 81 | handlers = console 82 | qualname = 83 | 84 | [logger_sqlalchemy] 85 | level = WARN 86 | handlers = 87 | qualname = sqlalchemy.engine 88 | 89 | [logger_alembic] 90 | level = INFO 91 | handlers = 92 | qualname = alembic 93 | 94 | [handler_console] 95 | class = StreamHandler 96 | args = (sys.stderr,) 97 | level = NOTSET 98 | formatter = generic 99 | 100 | [formatter_generic] 101 | format = %(levelname)-5.5s [%(name)s] %(message)s 102 | datefmt = %H:%M:%S 103 | -------------------------------------------------------------------------------- /endpoints/gem_endpoints.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Union 2 | 3 | from fastapi import APIRouter, Security, security, Depends, Query 4 | from fastapi.security import HTTPAuthorizationCredentials 5 | from sqlmodel import select 6 | from starlette.responses import JSONResponse 7 | from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_401_UNAUTHORIZED 8 | from fastapi.encoders import jsonable_encoder 9 | import repos.gem_repository 10 | from endpoints.user_endpoints import auth_handler 11 | from populate import calculate_gem_price 12 | from models.gem_models import * 13 | from db.db import session 14 | 15 | gem_router = APIRouter() 16 | 17 | 18 | @gem_router.get('/') 19 | def greet(): 20 | return 'Hello production' 21 | 22 | 23 | @gem_router.get('/gems', tags=['Gems']) 24 | def gems(lte: Optional[int] = None, gte: Optional[int] = None, 25 | type: List[Optional[GemTypes]] = Query(None)): 26 | gems = select(Gem, GemProperties).join(GemProperties) 27 | if lte: 28 | gems = gems.where(Gem.price <= lte) 29 | if gte: 30 | gems = gems.where(Gem.price >= gte) 31 | if type: 32 | gems = gems.where(Gem.gem_type.in_(type)).order_by(Gem.gem_type).order_by(-Gem.price).order_by(None) 33 | gems = session.exec(gems).all() 34 | return {'gems': gems} 35 | 36 | 37 | @gem_router.get('/gem/{id}', response_model=Gem, tags=['Gems']) 38 | def gem(id: int): 39 | gem_found = session.get(Gem, id) 40 | if not gem_found: 41 | return JSONResponse(status_code=HTTP_404_NOT_FOUND) 42 | return gem_found 43 | 44 | 45 | @gem_router.post('/gems', tags=['Gems']) 46 | def create_gem(gem_pr: GemProperties, gem: Gem, user=Depends(auth_handler.get_current_user)): 47 | """Creates gem""" 48 | if not user.is_seller: 49 | return JSONResponse(status_code=HTTP_401_UNAUTHORIZED) 50 | 51 | gem_properties = GemProperties(size=gem_pr.size, clarity=gem_pr.clarity, 52 | color=gem_pr.color) 53 | session.add(gem_properties) 54 | session.commit() 55 | gem_ = Gem(price=gem.price, available=gem.available, gem_properties=gem_properties, 56 | gem_properties_id=gem_properties.id, seller_id=user.id, seller=user) 57 | price = calculate_gem_price(gem, gem_pr) 58 | gem_.price = price 59 | session.add(gem_) 60 | session.commit() 61 | return gem 62 | 63 | 64 | @gem_router.put('/gems/{id}', response_model=Gem, tags=['Gems']) 65 | def update_gem(id: int, gem: Gem, user=Depends(auth_handler.get_current_user)): 66 | gem_found = session.get(Gem, id) 67 | if not user.is_seller or gem_found.seller_id != user.id: 68 | return JSONResponse(status_code=HTTP_401_UNAUTHORIZED) 69 | update_item_encoded = jsonable_encoder(gem) 70 | update_item_encoded.pop('id', None) 71 | for key, val in update_item_encoded.items(): 72 | gem_found.__setattr__(key, val) 73 | session.commit() 74 | return gem_found 75 | 76 | 77 | @gem_router.patch('/gems/{id}', response_model=Gem, tags=['Gems']) 78 | def patch_gem(id: int, gem: GemPatch, user=Depends(auth_handler.get_current_user)): 79 | gem_found = session.get(Gem, id) 80 | if not user.is_seller or gem_found.seller_id != user.id: 81 | return JSONResponse(status_code=HTTP_401_UNAUTHORIZED) 82 | update_data = gem.dict(exclude_unset=True) 83 | update_data.pop('id', None) 84 | for key, val in update_data.items(): 85 | gem_found.__setattr__(key, val) 86 | session.commit() 87 | return gem_found 88 | 89 | 90 | @gem_router.delete('/gems/{id}', status_code=HTTP_204_NO_CONTENT, tags=['Gems']) 91 | def delete_gem(id:int, user=Depends(auth_handler.get_current_user)): 92 | gem_found = session.get(Gem, id) 93 | if not user.is_seller or gem_found.seller_id != user.id: 94 | return JSONResponse(status_code=HTTP_401_UNAUTHORIZED) 95 | session.delete(gem_found) 96 | session.commit() 97 | 98 | 99 | @gem_router.get('/gems/seller/me', tags=['seller'], 100 | response_model=List[Dict[str, Union[Gem, GemProperties]]]) 101 | def gems_seller(user=Depends(auth_handler.get_current_user)): 102 | if not user.is_seller: 103 | return JSONResponse(status_code=HTTP_401_UNAUTHORIZED) 104 | statement = select(Gem, GemProperties).where(Gem.seller_id == user.id).join(GemProperties) 105 | gems = session.exec(statement).all() 106 | res = [{'gem': gem, 'props': props} for gem, props in gems] 107 | return res --------------------------------------------------------------------------------