├── .env ├── .idea ├── .gitignore ├── clothesExample.iml ├── dataSources.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml └── modules.xml ├── __pycache__ └── main.cpython-37.pyc ├── alembic.ini ├── clean_code.py ├── main.py ├── migrations ├── README ├── __pycache__ │ └── env.cpython-37.pyc ├── env.py ├── script.py.mako └── versions │ ├── 0577af2d08be_add_user_role.py │ ├── 744a9fdfa7a9_initial_migration.py │ └── __pycache__ │ ├── 0577af2d08be_add_user_role.cpython-37.pyc │ └── 744a9fdfa7a9_initial_migration.cpython-37.pyc └── test_main.http /.env: -------------------------------------------------------------------------------- 1 | DB_USER=postgres 2 | DB_PASSWORD=ines123 3 | JWT_SECRET=supersecret12336545 -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/clothesExample.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | postgresql 6 | true 7 | org.postgresql.Driver 8 | jdbc:postgresql://localhost:5433/postgres 9 | $ProjectFileDir$ 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /__pycache__/main.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InesIvanova/FastAPIclothesApp/63e05e2986d80564bf2c70607308c100b84930d7/__pycache__/main.cpython-37.pyc -------------------------------------------------------------------------------- /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" 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. Valid values are: 43 | # 44 | # version_path_separator = : 45 | # version_path_separator = ; 46 | # version_path_separator = space 47 | version_path_separator = os # default: use os.pathsep 48 | 49 | # the output encoding used when revision files 50 | # are written from script.py.mako 51 | # output_encoding = utf-8 52 | 53 | sqlalchemy.url = postgresql://postgres:ines123@localhost:5433/clothes 54 | 55 | 56 | [post_write_hooks] 57 | # post_write_hooks defines scripts or Python functions that are run 58 | # on newly generated revision scripts. See the documentation for further 59 | # detail and examples 60 | 61 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 62 | # hooks = black 63 | # black.type = console_scripts 64 | # black.entrypoint = black 65 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 66 | 67 | # Logging configuration 68 | [loggers] 69 | keys = root,sqlalchemy,alembic 70 | 71 | [handlers] 72 | keys = console 73 | 74 | [formatters] 75 | keys = generic 76 | 77 | [logger_root] 78 | level = WARN 79 | handlers = console 80 | qualname = 81 | 82 | [logger_sqlalchemy] 83 | level = WARN 84 | handlers = 85 | qualname = sqlalchemy.engine 86 | 87 | [logger_alembic] 88 | level = INFO 89 | handlers = 90 | qualname = alembic 91 | 92 | [handler_console] 93 | class = StreamHandler 94 | args = (sys.stderr,) 95 | level = NOTSET 96 | formatter = generic 97 | 98 | [formatter_generic] 99 | format = %(levelname)-5.5s [%(name)s] %(message)s 100 | datefmt = %H:%M:%S 101 | -------------------------------------------------------------------------------- /clean_code.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | FILE_PATH = "../../temp" 4 | 5 | 6 | class SimpleMath: 7 | @staticmethod 8 | def my_first_method(a: int, b: int = 5) -> None: 9 | # This is needed because ... 10 | a = 6 11 | b = 7 12 | 13 | math.sqrt(a) 14 | 15 | def second_method(self) -> int: 16 | """ 17 | This method represents the business logic for ... 18 | It integrates AWS for uploading big images to the bucket. 19 | ... 20 | """ 21 | return 5 22 | 23 | 24 | my_class = SimpleMath() 25 | c = my_class.second_method() 26 | c.upgrade({"a": 4}) 27 | 28 | a_var = 5 29 | if a_var == 5: 30 | print("True") 31 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Optional 3 | 4 | import databases 5 | import enum 6 | 7 | import jwt 8 | import sqlalchemy 9 | from h11._abnf import status_code 10 | from pydantic import BaseModel, validator 11 | from fastapi import FastAPI, HTTPException, dependencies, Depends 12 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials 13 | from decouple import config 14 | from email_validator import validate_email as validate_e, EmailNotValidError 15 | from passlib.context import CryptContext 16 | from starlette.requests import Request 17 | 18 | DATABASE_URL = f"postgresql://{config('DB_USER')}:{config('DB_PASSWORD')}@localhost:5433/clothes" 19 | 20 | database = databases.Database(DATABASE_URL) 21 | 22 | metadata = sqlalchemy.MetaData() 23 | 24 | 25 | class UserRole(enum.Enum): 26 | super_admin = "super admin" 27 | admin = "admin" 28 | user = "user" 29 | 30 | 31 | users = sqlalchemy.Table( 32 | "users", 33 | metadata, 34 | sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True), 35 | sqlalchemy.Column("email", sqlalchemy.String(120), unique=True), 36 | sqlalchemy.Column("password", sqlalchemy.String(255)), 37 | sqlalchemy.Column("full_name", sqlalchemy.String(200)), 38 | sqlalchemy.Column("phone", sqlalchemy.String(13)), 39 | sqlalchemy.Column("created_at", sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()), 40 | sqlalchemy.Column( 41 | "last_modified_at", 42 | sqlalchemy.DateTime, 43 | nullable=False, 44 | server_default=sqlalchemy.func.now(), 45 | onupdate=sqlalchemy.func.now(), 46 | ), 47 | sqlalchemy.Column("role", sqlalchemy.Enum(UserRole), nullable=False, server_default=UserRole.user.name) 48 | ) 49 | 50 | 51 | class ColorEnum(enum.Enum): 52 | pink = "pink" 53 | black = "black" 54 | white = "white" 55 | yellow = "yellow" 56 | 57 | 58 | class SizeEnum(enum.Enum): 59 | xs = "xs" 60 | s = "s" 61 | m = "m" 62 | l = "l" 63 | xl = "xl" 64 | xxl = "xxl" 65 | 66 | 67 | clothes = sqlalchemy.Table( 68 | "clothes", 69 | metadata, 70 | sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True), 71 | sqlalchemy.Column("name", sqlalchemy.String(120)), 72 | sqlalchemy.Column("color", sqlalchemy.Enum(ColorEnum), nullable=False), 73 | sqlalchemy.Column("size", sqlalchemy.Enum(SizeEnum), nullable=False), 74 | sqlalchemy.Column("photo_url", sqlalchemy.String(255)), 75 | sqlalchemy.Column("created_at", sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()), 76 | sqlalchemy.Column( 77 | "last_modified_at", 78 | sqlalchemy.DateTime, 79 | nullable=False, 80 | server_default=sqlalchemy.func.now(), 81 | onupdate=sqlalchemy.func.now(), 82 | ), 83 | ) 84 | 85 | 86 | class EmailField(str): 87 | @classmethod 88 | def __get_validators__(cls): 89 | yield cls.validate 90 | 91 | @classmethod 92 | def validate(cls, v) -> str: 93 | try: 94 | validate_e(v) 95 | return v 96 | except EmailNotValidError: 97 | raise ValueError("Email is not valid") 98 | 99 | 100 | class BaseUser(BaseModel): 101 | email: str 102 | full_name: Optional[str] 103 | 104 | @validator("full_name") 105 | def validate_full_name(cls, v): 106 | try: 107 | first_name, last_name = v.split() 108 | return v 109 | except Exception: 110 | raise ValueError("You should provide at least 2 names") 111 | 112 | 113 | class UserSignIn(BaseUser): 114 | password: str 115 | 116 | 117 | class UserSignOut(BaseUser): 118 | phone: Optional[str] 119 | created_at: datetime 120 | last_modified_at: datetime 121 | 122 | 123 | app = FastAPI() 124 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 125 | 126 | 127 | class CustomHTTPBearer(HTTPBearer): 128 | async def __call__( 129 | self, request: Request 130 | ) -> Optional[HTTPAuthorizationCredentials]: 131 | res = await super().__call__(request) 132 | 133 | try: 134 | payload = jwt.decode(res.credentials, config("JWT_SECRET"), algorithms=["HS256"]) 135 | user = await database.fetch_one(users.select().where(users.c.id == payload["sub"])) 136 | request.state.user = user 137 | return payload 138 | except jwt.ExpiredSignatureError: 139 | raise HTTPException(401, "Token is expired") 140 | except jwt.InvalidTokenError: 141 | raise HTTPException(401, "Invalid token") 142 | 143 | 144 | oauth2_scheme = CustomHTTPBearer() 145 | 146 | 147 | def is_admin(request: Request): 148 | 149 | user = request.state.user 150 | if not user or user["role"] not in (UserRole.admin, UserRole.super_admin): 151 | raise HTTPException(403, "You do not have permissions for this resource") 152 | 153 | 154 | def create_access_token(user): 155 | try: 156 | payload = {"sub": user["id"], "exp": datetime.utcnow() + timedelta(minutes=120)} 157 | return jwt.encode(payload, config("JWT_SECRET"), algorithm="HS256") 158 | except Exception as ex: 159 | raise ex 160 | 161 | 162 | @app.on_event("startup") 163 | async def startup(): 164 | await database.connect() 165 | 166 | 167 | @app.on_event("shutdown") 168 | async def shutdown(): 169 | await database.disconnect() 170 | 171 | 172 | @app.get("/clothes/", dependencies=[Depends(oauth2_scheme)]) 173 | async def get_all_clothes(request: Request): 174 | user = request.state.user 175 | return await database.fetch_all(clothes.select()) 176 | 177 | 178 | class ClothesBase(BaseModel): 179 | name: str 180 | color: str 181 | size: SizeEnum 182 | color: ColorEnum 183 | 184 | 185 | class ClothesIn(ClothesBase): 186 | pass 187 | 188 | 189 | class ClothesOut(ClothesBase): 190 | id: int 191 | created_at: datetime 192 | last_modified_at: datetime 193 | 194 | 195 | 196 | @app.post("/clothes/", 197 | response_model=ClothesOut, 198 | dependencies=[Depends(oauth2_scheme), 199 | Depends(is_admin)], 200 | status_code=201 201 | ) 202 | async def create_clothes(clothes_data: ClothesIn): 203 | id_ = await database.execute(clothes.insert().values(**clothes_data.dict())) 204 | return await database.fetch_one(clothes.select().where(clothes.c.id == id_)) 205 | 206 | 207 | @app.post("/register/", status_code=201) 208 | async def create_user(user: UserSignIn): 209 | user.password = pwd_context.hash(user.password) 210 | q = users.insert().values(**user.dict()) 211 | id_ = await database.execute(q) 212 | created_user = await database.fetch_one(users.select().where(users.c.id == id_)) 213 | token = create_access_token(created_user) 214 | return {"token": token} -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/__pycache__/env.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InesIvanova/FastAPIclothesApp/63e05e2986d80564bf2c70607308c100b84930d7/migrations/__pycache__/env.cpython-37.pyc -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | from main import metadata 8 | 9 | # this is the Alembic Config object, which provides 10 | # access to the values within the .ini file in use. 11 | config = context.config 12 | 13 | # Interpret the config file for Python logging. 14 | # This line sets up loggers basically. 15 | fileConfig(config.config_file_name) 16 | 17 | # add your model's MetaData object here 18 | # for 'autogenerate' support 19 | # from myapp import mymodel 20 | # target_metadata = mymodel.Base.metadata 21 | target_metadata = metadata 22 | 23 | # other values from the config, defined by the needs of env.py, 24 | # can be acquired: 25 | # my_important_option = config.get_main_option("my_important_option") 26 | # ... etc. 27 | 28 | 29 | def run_migrations_offline(): 30 | """Run migrations in 'offline' mode. 31 | 32 | This configures the context with just a URL 33 | and not an Engine, though an Engine is acceptable 34 | here as well. By skipping the Engine creation 35 | we don't even need a DBAPI to be available. 36 | 37 | Calls to context.execute() here emit the given string to the 38 | script output. 39 | 40 | """ 41 | url = config.get_main_option("sqlalchemy.url") 42 | context.configure( 43 | url=url, 44 | target_metadata=target_metadata, 45 | literal_binds=True, 46 | dialect_opts={"paramstyle": "named"}, 47 | ) 48 | 49 | with context.begin_transaction(): 50 | context.run_migrations() 51 | 52 | 53 | def run_migrations_online(): 54 | """Run migrations in 'online' mode. 55 | 56 | In this scenario we need to create an Engine 57 | and associate a connection with the context. 58 | 59 | """ 60 | connectable = engine_from_config( 61 | config.get_section(config.config_ini_section), 62 | prefix="sqlalchemy.", 63 | poolclass=pool.NullPool, 64 | ) 65 | 66 | with connectable.connect() as connection: 67 | context.configure( 68 | connection=connection, target_metadata=target_metadata 69 | ) 70 | 71 | with context.begin_transaction(): 72 | context.run_migrations() 73 | 74 | 75 | if context.is_offline_mode(): 76 | run_migrations_offline() 77 | else: 78 | run_migrations_online() 79 | -------------------------------------------------------------------------------- /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 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/0577af2d08be_add_user_role.py: -------------------------------------------------------------------------------- 1 | """add user role 2 | 3 | Revision ID: 0577af2d08be 4 | Revises: 744a9fdfa7a9 5 | Create Date: 2022-01-01 13:11:33.542504 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import postgresql 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = '0577af2d08be' 15 | down_revision = '744a9fdfa7a9' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | # ### commands auto generated by Alembic - please adjust! ### 22 | user_role = postgresql.ENUM('super_admin', 'admin', 'user', name="user_role") 23 | user_role.create(op.get_bind()) 24 | op.add_column('users', sa.Column('role', sa.Enum('super_admin', 'admin', 'user', name='user_role'), server_default='user', nullable=False)) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_column('users', 'role') 31 | # ### end Alembic commands ### 32 | -------------------------------------------------------------------------------- /migrations/versions/744a9fdfa7a9_initial_migration.py: -------------------------------------------------------------------------------- 1 | """Initial migration 2 | 3 | Revision ID: 744a9fdfa7a9 4 | Revises: 5 | Create Date: 2022-01-01 11:17:33.807703 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '744a9fdfa7a9' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('clothes', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('name', sa.String(length=120), nullable=True), 24 | sa.Column('color', sa.Enum('pink', 'black', 'white', 'yellow', name='colorenum'), nullable=False), 25 | sa.Column('size', sa.Enum('xs', 's', 'm', 'l', 'xl', 'xxl', name='sizeenum'), nullable=False), 26 | sa.Column('photo_url', sa.String(length=255), nullable=True), 27 | sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), 28 | sa.Column('last_modified_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), 29 | sa.PrimaryKeyConstraint('id') 30 | ) 31 | op.create_table('users', 32 | sa.Column('id', sa.Integer(), nullable=False), 33 | sa.Column('email', sa.String(length=120), nullable=True), 34 | sa.Column('password', sa.String(length=255), nullable=True), 35 | sa.Column('full_name', sa.String(length=200), nullable=True), 36 | sa.Column('phone', sa.String(length=13), nullable=True), 37 | sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), 38 | sa.Column('last_modified_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), 39 | sa.PrimaryKeyConstraint('id'), 40 | sa.UniqueConstraint('email') 41 | ) 42 | # ### end Alembic commands ### 43 | 44 | 45 | def downgrade(): 46 | # ### commands auto generated by Alembic - please adjust! ### 47 | op.drop_table('users') 48 | op.drop_table('clothes') 49 | # ### end Alembic commands ### 50 | -------------------------------------------------------------------------------- /migrations/versions/__pycache__/0577af2d08be_add_user_role.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InesIvanova/FastAPIclothesApp/63e05e2986d80564bf2c70607308c100b84930d7/migrations/versions/__pycache__/0577af2d08be_add_user_role.cpython-37.pyc -------------------------------------------------------------------------------- /migrations/versions/__pycache__/744a9fdfa7a9_initial_migration.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InesIvanova/FastAPIclothesApp/63e05e2986d80564bf2c70607308c100b84930d7/migrations/versions/__pycache__/744a9fdfa7a9_initial_migration.cpython-37.pyc -------------------------------------------------------------------------------- /test_main.http: -------------------------------------------------------------------------------- 1 | # Test your FastAPI endpoints 2 | 3 | GET http://127.0.0.1:8000/ 4 | Accept: application/json 5 | 6 | ### 7 | 8 | GET http://127.0.0.1:8000/hello/User 9 | Accept: application/json 10 | 11 | ### 12 | --------------------------------------------------------------------------------