├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── alembic.ini ├── alembic ├── README ├── env.py ├── script.py.mako └── versions │ └── e246fb3eaee6_create_user_table.py ├── db_initializer.py ├── docker-compose.yml ├── main.py ├── models ├── __init__.py └── users.py ├── requirements.txt ├── schemas ├── __init__.py └── users.py ├── services ├── __init__.py └── db │ ├── __init__.py │ └── users.py └── settings.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | 3 | ENV PYTHONUNBUFFERED=1 4 | 5 | WORKDIR /home 6 | 7 | COPY ./requirements.txt . 8 | 9 | RUN pip install -r requirements.txt \ 10 | && adduser --disabled-password --no-create-home doe 11 | 12 | COPY * . 13 | 14 | USER doe 15 | 16 | EXPOSE 8000 17 | 18 | CMD ["uvicorn", "main:app", "--port", "8000", "--host", "0.0.0.0"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Agbonze Osazuwa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # learn-fastapi 2 | My documented journey to learning fastapi 3 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file 10 | # for all available tokens 11 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 12 | 13 | # sys.path path, will be prepended to sys.path if present. 14 | # defaults to the current working directory. 15 | prepend_sys_path = . 16 | 17 | # timezone to use when rendering the date within the migration file 18 | # as well as the filename. 19 | # If specified, requires the python-dateutil library that can be 20 | # installed by adding `alembic[tz]` to the pip requirements 21 | # string value is passed to dateutil.tz.gettz() 22 | # leave blank for localtime 23 | # timezone = 24 | 25 | # max length of characters to apply to the 26 | # "slug" field 27 | # truncate_slug_length = 40 28 | 29 | # set to 'true' to run the environment during 30 | # the 'revision' command, regardless of autogenerate 31 | # revision_environment = false 32 | 33 | # set to 'true' to allow .pyc and .pyo files without 34 | # a source .py file to be detected as revisions in the 35 | # versions/ directory 36 | # sourceless = false 37 | 38 | # version location specification; This defaults 39 | # to alembic/versions. When using multiple version 40 | # directories, initial revisions must be specified with --version-path. 41 | # The path separator used here should be the separator specified by "version_path_separator" below. 42 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 43 | 44 | # version path separator; As mentioned above, this is the character used to split 45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 47 | # Valid values for version_path_separator are: 48 | # 49 | # version_path_separator = : 50 | # version_path_separator = ; 51 | # version_path_separator = space 52 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 53 | 54 | # the output encoding used when revision files 55 | # are written from script.py.mako 56 | # output_encoding = utf-8 57 | 58 | sqlalchemy.url = driver://user:pass@localhost/dbname 59 | 60 | 61 | [post_write_hooks] 62 | # post_write_hooks defines scripts or Python functions that are run 63 | # on newly generated revision scripts. See the documentation for further 64 | # detail and examples 65 | 66 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 67 | # hooks = black 68 | # black.type = console_scripts 69 | # black.entrypoint = black 70 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 71 | 72 | # Logging configuration 73 | [loggers] 74 | keys = root,sqlalchemy,alembic 75 | 76 | [handlers] 77 | keys = console 78 | 79 | [formatters] 80 | keys = generic 81 | 82 | [logger_root] 83 | level = WARN 84 | handlers = console 85 | qualname = 86 | 87 | [logger_sqlalchemy] 88 | level = WARN 89 | handlers = 90 | qualname = sqlalchemy.engine 91 | 92 | [logger_alembic] 93 | level = INFO 94 | handlers = 95 | qualname = alembic 96 | 97 | [handler_console] 98 | class = StreamHandler 99 | args = (sys.stderr,) 100 | level = NOTSET 101 | formatter = generic 102 | 103 | [formatter_generic] 104 | format = %(levelname)-5.5s [%(name)s] %(message)s 105 | datefmt = %H:%M:%S 106 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /alembic/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 | 8 | from db_initializer import Base 9 | from settings import DATABASE_URL 10 | 11 | from models.users import User 12 | 13 | 14 | 15 | 16 | # this is the Alembic Config object, which provides 17 | # access to the values within the .ini file in use. 18 | config = context.config 19 | 20 | # Interpret the config file for Python logging. 21 | # This line sets up loggers basically. 22 | if config.config_file_name is not None: 23 | fileConfig(config.config_file_name) 24 | 25 | # add your model's MetaData object here 26 | # for 'autogenerate' support 27 | # from myapp import mymodel 28 | # target_metadata = mymodel.Base.metadata 29 | target_metadata = Base.metadata 30 | 31 | # other values from the config, defined by the needs of env.py, 32 | # can be acquired: 33 | # my_important_option = config.get_main_option("my_important_option") 34 | # ... etc. 35 | config.set_section_option(config.config_ini_section, "sqlalchemy.url", DATABASE_URL) 36 | 37 | 38 | def run_migrations_offline() -> None: 39 | """Run migrations in 'offline' mode. 40 | 41 | This configures the context with just a URL 42 | and not an Engine, though an Engine is acceptable 43 | here as well. By skipping the Engine creation 44 | we don't even need a DBAPI to be available. 45 | 46 | Calls to context.execute() here emit the given string to the 47 | script output. 48 | 49 | """ 50 | url = config.get_main_option("sqlalchemy.url") 51 | context.configure( 52 | url=url, 53 | compare_type=True, 54 | literal_binds=True, 55 | target_metadata=target_metadata, 56 | dialect_opts={"paramstyle": "named"}, 57 | ) 58 | 59 | with context.begin_transaction(): 60 | context.run_migrations() 61 | 62 | 63 | def run_migrations_online() -> None: 64 | """Run migrations in 'online' mode. 65 | 66 | In this scenario we need to create an Engine 67 | and associate a connection with the context. 68 | 69 | """ 70 | connectable = engine_from_config( 71 | config.get_section(config.config_ini_section), 72 | prefix="sqlalchemy.", 73 | poolclass=pool.NullPool, 74 | ) 75 | 76 | with connectable.connect() as connection: 77 | context.configure( 78 | compare_type=True, 79 | connection=connection, 80 | target_metadata=target_metadata, 81 | ) 82 | 83 | with context.begin_transaction(): 84 | context.run_migrations() 85 | 86 | 87 | if context.is_offline_mode(): 88 | run_migrations_offline() 89 | else: 90 | run_migrations_online() 91 | -------------------------------------------------------------------------------- /alembic/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() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /alembic/versions/e246fb3eaee6_create_user_table.py: -------------------------------------------------------------------------------- 1 | """Create user table 2 | 3 | Revision ID: e246fb3eaee6 4 | Revises: 5 | Create Date: 2023-01-04 18:13:04.457135 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e246fb3eaee6' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('users', 22 | sa.Column('email', sa.String(length=225), nullable=False), 23 | sa.Column('id', sa.Integer(), nullable=False), 24 | sa.Column('hashed_password', sa.LargeBinary(), nullable=False), 25 | sa.Column('full_name', sa.String(length=225), nullable=False), 26 | sa.Column('is_active', sa.Boolean(), nullable=True), 27 | sa.PrimaryKeyConstraint('id'), 28 | sa.UniqueConstraint('email') 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade() -> None: 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('users') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /db_initializer.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker, declarative_base 3 | 4 | import settings 5 | 6 | 7 | # Create database engine 8 | engine = create_engine(settings.DATABASE_URL, echo=True, future=True) 9 | 10 | # Create database declarative base 11 | Base = declarative_base() 12 | 13 | # Create session 14 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 15 | 16 | 17 | def get_db(): 18 | """Database session generator""" 19 | db = SessionLocal() 20 | try: 21 | yield db 22 | finally: 23 | db.close() 24 | 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | db: 5 | image: postgres:12-alpine 6 | container_name: fastapiapp_demodb 7 | restart: always 8 | environment: 9 | - POSTGRES_DB=$POSTGRES_DB 10 | - POSTGRES_USER=$POSTGRES_USER 11 | - POSTGRES_PASSWORD=$POSTGRES_PASSWORD 12 | networks: 13 | - fastapiappnetwork 14 | 15 | app: 16 | build: . 17 | container_name: fastapiapp_demoapp 18 | ports: 19 | - 8000:8000 20 | volumes: 21 | - .:/home 22 | depends_on: 23 | - db 24 | restart: always 25 | networks: 26 | - fastapiappnetwork 27 | environment: 28 | - POSTGRES_DB=$POSTGRES_DB 29 | - POSTGRES_PORT=$POSTGRES_PORT 30 | - POSTGRES_HOST=$POSTGRES_HOST 31 | - POSTGRES_USER=$POSTGRES_USER 32 | - POSTGRES_PASSWORD=$POSTGRES_PASSWORD 33 | - SECRET_KEY=$SECRET_KEY 34 | - CLOUDINARY_URL=$CLOUDINARY_URL 35 | 36 | networks: 37 | fastapiappnetwork: 38 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from fastapi import ( 4 | status, 5 | 6 | HTTPException, 7 | UploadFile, 8 | FastAPI, 9 | Depends, 10 | File, 11 | Body 12 | ) 13 | from fastapi.security import OAuth2PasswordBearer 14 | from fastapi.security import OAuth2PasswordRequestForm 15 | 16 | from sqlalchemy.orm import Session 17 | 18 | import cloudinary 19 | import cloudinary.uploader 20 | 21 | from db_initializer import get_db 22 | from models import users as user_model 23 | from services.db import users as user_db_services 24 | from schemas.users import ( 25 | CreateUserSchema, 26 | UserLoginSchema, 27 | UserSchema 28 | ) 29 | 30 | app = FastAPI() 31 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") 32 | 33 | 34 | @app.post('/login', response_model=Dict) 35 | def login( 36 | payload: OAuth2PasswordRequestForm = Depends(), 37 | session: Session = Depends(get_db) 38 | ): 39 | """Processes user's authentication and returns a token 40 | on successful authentication. 41 | 42 | request body: 43 | 44 | - username: Unique identifier for a user e.g email, 45 | phone number, name 46 | 47 | - password: 48 | """ 49 | try: 50 | user:user_model.User = user_db_services.get_user( 51 | session=session, email=payload.username 52 | ) 53 | except: 54 | raise HTTPException( 55 | status_code=status.HTTP_401_UNAUTHORIZED, 56 | detail="Invalid user credentials" 57 | ) 58 | 59 | is_validated:bool = user.validate_password(payload.password) 60 | if not is_validated: 61 | raise HTTPException( 62 | status_code=status.HTTP_401_UNAUTHORIZED, 63 | detail="Invalid user credentials" 64 | ) 65 | 66 | return user.generate_token() 67 | 68 | 69 | @app.post('/signup', response_model=UserSchema) 70 | def signup( 71 | payload: CreateUserSchema = Body(), 72 | session:Session=Depends(get_db) 73 | ): 74 | """Processes request to register user account.""" 75 | payload.hashed_password = user_model.User.hash_password(payload.hashed_password) 76 | return user_db_services.create_user(session, user=payload) 77 | 78 | 79 | @app.get("/profile/{id}", response_model=UserSchema) 80 | def profile( 81 | id:int, 82 | session:Session=Depends(get_db), 83 | token: str = Depends(oauth2_scheme), 84 | ): 85 | """Processes request to retrieve the requesting user 86 | profile 87 | """ 88 | return user_db_services.get_user_by_id(session=session, id=id) 89 | 90 | 91 | @app.post('/upload-profile-image', response_model=str) 92 | def upload_profile_image( 93 | # token: str = Depends(oauth2_scheme), 94 | file:UploadFile = File(description="User profile image"), 95 | ): 96 | """Processes request to upload profile image""" 97 | # utilizes cloudinary to upload profile 98 | # collect image url and save to db 99 | # return response 100 | cloudinary.uploader.upload( 101 | file.file, 102 | overwrite=True, 103 | unique_filename=False, 104 | public_id="test_image", 105 | ) 106 | 107 | image_url = cloudinary.CloudinaryImage("test_image").build_url() 108 | return image_url -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spaceofmiah/learn-fastapi/21dcdee4edb17e2458a620ac538d15f292a53b1f/models/__init__.py -------------------------------------------------------------------------------- /models/users.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | LargeBinary, 3 | Column, 4 | String, 5 | Integer, 6 | Boolean, 7 | UniqueConstraint, 8 | PrimaryKeyConstraint 9 | ) 10 | 11 | import jwt 12 | import bcrypt 13 | 14 | from db_initializer import Base 15 | 16 | import settings 17 | 18 | 19 | 20 | class User(Base): 21 | """Models a user table""" 22 | __tablename__ = "users" 23 | email = Column(String(225), nullable=False, unique=True) 24 | id = Column(Integer, nullable=False, primary_key=True) 25 | hashed_password = Column(LargeBinary, nullable=False) 26 | full_name = Column(String(225), nullable=False) 27 | is_active = Column(Boolean, default=False) 28 | 29 | UniqueConstraint("email", name="uq_user_email") 30 | PrimaryKeyConstraint("id", name="pk_user_id") 31 | 32 | def __repr__(self): 33 | """Returns string representation of model instance""" 34 | return "".format(full_name=self.full_name) 35 | 36 | @staticmethod 37 | def hash_password(password) -> str: 38 | """Transforms password from it's raw textual form to 39 | cryptographic hashes 40 | """ 41 | return bcrypt.hashpw(password.encode(), bcrypt.gensalt()) 42 | 43 | def validate_password(self, password) -> bool: 44 | """Confirms password validity""" 45 | return bcrypt.checkpw(password.encode(), self.hashed_password) 46 | 47 | def generate_token(self) -> dict: 48 | """Generate access token for user""" 49 | return { 50 | "access_token": jwt.encode( 51 | {"full_name": self.full_name, "email": self.email}, 52 | settings.SECRET_KEY 53 | ) 54 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.88.0 2 | bcrypt==4.0.1 3 | pyjwt==2.6.0 4 | alembic>=1.9.1 5 | uvicorn==0.20.0 6 | cloudinary==1.31.0 7 | SQLAlchemy>=1.4,<=2.0 8 | psycopg2-binary==2.9.5 9 | email-validator>=1.0.3 10 | python-multipart==0.0.5 -------------------------------------------------------------------------------- /schemas/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spaceofmiah/learn-fastapi/21dcdee4edb17e2458a620ac538d15f292a53b1f/schemas/__init__.py -------------------------------------------------------------------------------- /schemas/users.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, EmailStr 2 | 3 | 4 | 5 | class UserBaseSchema(BaseModel): 6 | email: EmailStr 7 | full_name: str 8 | 9 | 10 | class CreateUserSchema(UserBaseSchema): 11 | hashed_password: str = Field(alias="password") 12 | 13 | 14 | class UserLoginSchema(BaseModel): 15 | email: EmailStr = Field(alias="username") 16 | password: str 17 | 18 | 19 | class UserSchema(UserBaseSchema): 20 | id: int 21 | is_active: bool = Field(default=False) 22 | 23 | class Config: 24 | orm_mode = True 25 | 26 | 27 | -------------------------------------------------------------------------------- /services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spaceofmiah/learn-fastapi/21dcdee4edb17e2458a620ac538d15f292a53b1f/services/__init__.py -------------------------------------------------------------------------------- /services/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spaceofmiah/learn-fastapi/21dcdee4edb17e2458a620ac538d15f292a53b1f/services/db/__init__.py -------------------------------------------------------------------------------- /services/db/users.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from sqlalchemy import select 3 | 4 | from models.users import User 5 | from schemas.users import CreateUserSchema 6 | 7 | 8 | 9 | def create_user(session:Session, user:CreateUserSchema): 10 | db_user = User(**user.dict()) 11 | session.add(db_user) 12 | session.commit() 13 | session.refresh(db_user) 14 | return db_user 15 | 16 | 17 | def list_users(session:Session): 18 | return session.query(User).all() 19 | 20 | 21 | def get_user(session:Session, email:str): 22 | return session.query(User).filter(User.email == email).one() 23 | 24 | 25 | def get_user_by_id(session:Session, id:int): 26 | return session.query(User).filter(User.id == id).one() 27 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cloudinary 3 | 4 | # Database url configuration 5 | DATABASE_URL = "postgresql+psycopg2://{username}:{password}@{host}:{port}/{db_name}".format( 6 | host=os.getenv("POSTGRES_HOST"), 7 | port=os.getenv("POSTGRES_PORT"), 8 | db_name=os.getenv("POSTGRES_DB"), 9 | username=os.getenv("POSTGRES_USER"), 10 | password=os.getenv("POSTGRES_PASSWORD"), 11 | ) 12 | 13 | config = cloudinary.config(secure=True) 14 | 15 | 16 | SECRET_KEY = os.getenv("SECRET_KEY") 17 | --------------------------------------------------------------------------------