├── .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 |
4 |
5 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
--------------------------------------------------------------------------------