├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── alembic.ini ├── alembic ├── README ├── env.py ├── script.py.mako └── versions │ ├── 38a01968532b_.py │ ├── 3ecb256a8f85_.py │ └── b04eb3b09276_.py ├── charts └── fastapi-helm │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── celery │ │ ├── deployment.yml │ │ └── secret.yml │ ├── code │ │ ├── deployment.yml │ │ ├── secret.yml │ │ └── service.yml │ ├── job │ │ ├── migration.yml │ │ └── secret.yml │ ├── nginx │ │ ├── nginx-config.yml │ │ ├── nginx-deployment.yml │ │ └── nginx-service.yml │ ├── postgres │ │ ├── postgres-deployment.yml │ │ ├── postgres-pv.yml │ │ ├── postgres-pvc.yml │ │ ├── postgres-secret.yml │ │ └── postgres-service.yml │ └── redis │ │ ├── deployment.yml │ │ └── service.yml │ └── values.yaml ├── conf_test_db.py ├── ecommerce ├── __init__.py ├── auth │ ├── __init__.py │ ├── jwt.py │ ├── router.py │ └── schema.py ├── cart │ ├── __init__.py │ ├── models.py │ ├── router.py │ ├── schema.py │ └── services.py ├── config.py ├── db.py ├── orders │ ├── __init__.py │ ├── mail.py │ ├── models.py │ ├── router.py │ ├── schema.py │ ├── services.py │ └── tasks.py ├── products │ ├── __init__.py │ ├── models.py │ ├── router.py │ ├── schema.py │ ├── services.py │ └── validator.py └── user │ ├── __init__.py │ ├── hashing.py │ ├── models.py │ ├── router.py │ ├── schema.py │ ├── services.py │ └── validator.py ├── eks ├── cluster.yml ├── deploy │ ├── celery │ │ ├── deployment.yml │ │ └── secret.yml │ ├── code │ │ ├── deployment.yml │ │ ├── secret.yml │ │ └── service.yml │ ├── elasticache │ │ └── redis-service.yml │ ├── ingress │ │ └── ingress.yml │ ├── job │ │ └── migration.yml │ └── rds │ │ └── db-service.yml └── utils │ ├── alb-ingress-controller.yaml │ ├── iam-policy.json │ └── rbac-role.yml ├── k8s ├── celery │ ├── deployment.yml │ └── secret.yml ├── code │ ├── deployment.yml │ ├── secret.yml │ └── service.yml ├── job │ ├── migration.yml │ └── secret.yml ├── namespace │ └── ns.yml ├── nginx │ ├── nginx-config.yml │ ├── nginx-deployment.yml │ └── nginx-service.yml ├── password-alter.txt ├── postgres │ ├── postgres-deployment.yml │ ├── postgres-pv.yml │ ├── postgres-pvc.yml │ ├── postgres-secret.yml │ └── postgres-service.yml └── redis │ ├── deployment.yml │ └── service.yml ├── main.py ├── misc └── images │ ├── celery-task.png │ ├── env_file.png │ ├── requirements.gif │ ├── stack.png │ └── testing.gif ├── requirements.txt └── tests ├── __init__.py ├── cart ├── __init__.py └── test_cart.py ├── conftest.py ├── home ├── __init__.py └── test_home.py ├── login ├── __init__.py └── test_login.py ├── orders ├── __init__.py └── test_orders.py ├── products ├── __init__.py ├── test_categories.py └── test_products.py ├── registration ├── __init__.py └── test_user_registration.py ├── shared ├── __init__.py └── info.py └── user ├── __init__.py └── test_user.py /.dockerignore: -------------------------------------------------------------------------------- 1 | eks 2 | k8s 3 | docs 4 | .pytest_cache 5 | README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | test.db 3 | .pytest_cache 4 | __pycache__/ 5 | .pyc 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM python:3.8.11-slim-buster 3 | 4 | # create directory for the app user 5 | RUN mkdir -p /home/app 6 | 7 | # create the app user 8 | RUN addgroup --system app && adduser --system --group app 9 | 10 | # create the appropriate directories 11 | ENV HOME=/home/app 12 | ENV APP_HOME=/home/app/code 13 | RUN mkdir $APP_HOME 14 | WORKDIR $APP_HOME 15 | 16 | # set environment variables 17 | ENV PYTHONDONTWRITEBYTECODE 1 18 | ENV PYTHONUNBUFFERED 1 19 | ENV ENVIRONMENT prod 20 | 21 | 22 | # install system dependencies 23 | RUN apt-get update \ 24 | && apt-get -y install netcat gcc libpq-dev \ 25 | && apt-get clean 26 | 27 | # install python dependencies 28 | RUN pip install --upgrade pip 29 | RUN pip install -U setuptools 30 | COPY ./requirements.txt . 31 | RUN pip install -r requirements.txt 32 | 33 | # add app 34 | COPY . . 35 | 36 | # chown all the files to the app user 37 | RUN chown -R app:app $APP_HOME 38 | 39 | # change to the app user 40 | USER app 41 | 42 | # run gunicorn 43 | CMD gunicorn --bind 0.0.0.0:5000 main:app -k uvicorn.workers.UvicornWorker -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI Tutorial Series 2 | 3 | ![stack_logo](https://gist.githubusercontent.com/mukulmantosh/d68428973c1a368ecc6f3781144abca8/raw/044c5e7b4e86d39c815d575ad63c56864e2a7dda/stack.png) 4 | 5 | 6 | Welcome to the FastAPI & Kubernetes Tutorial Series with PyCharm & AWS EKS. 7 | 8 | ## Prerequisites 9 | 10 | Before starting up this project, make sure you have an AWS account and 11 | PyCharm installed in your machine. 12 | 13 | * In this tutorial we will be using [PyCharm Professional](https://www.jetbrains.com/pycharm/). 14 | 15 | 16 | ### Software Installation 17 | 18 | - [x] [AWS Command Line Interface](https://aws.amazon.com/cli/) - The AWS Command Line Interface (CLI) is a unified tool to manage your AWS services. 19 | 20 | 21 | - [x] [eksctl](https://eksctl.io/) - The official CLI for Amazon EKS 22 | 23 | 24 | - [x] [Docker](https://www.docker.com/) - Docker helps developers bring their ideas to life by conquering the complexity of app development. 25 | 26 | 27 | - [x] [Kubernetes](https://kubernetes.io/) - also known as K8s, is an 28 | open-source system for automating deployment, scaling, and management of containerized applications. 29 | 30 | 31 | - [x] [Helm](https://helm.sh/) - The package manager for Kubernetes. Helm helps you manage 32 | Kubernetes applications — Helm Charts help you define, install, and upgrade even the most complex Kubernetes application. 33 | 34 | 35 | - [x] [PostgreSQL](https://www.postgresql.org/) - The World's Most Advanced Open Source Relational Database 36 | 37 | 38 | - [x] [Redis](https://redis.io/) - open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker 39 | 40 | 41 | - [x] [NICE DCV](https://www.nice-dcv.com/) (Optional) - Deliver high-performance remote desktop and application streaming. If 42 | you are interested to run your workload directly in AWS. 43 | 44 | ## System Dependencies 45 | 46 | - Make sure your system is up-to-date. 47 | - Run the below command to install python system 48 | dependencies along-with postgres driver. 49 | 50 | ```bash 51 | 52 | $ sudo apt-get install libpq-dev python-dev libssl-dev 53 | 54 | ``` 55 | 56 | 57 | 58 | ## Python Dependencies 59 | 60 | - Installing Python Packages 61 | 62 | ```bash 63 | 64 | $ pip install -r requirements.txt 65 | 66 | ``` 67 | 68 | ![requirements-install](./misc/images/requirements.gif) 69 | 70 | - Running Uvicorn Server 71 | 72 | ```bash 73 | 74 | $ uvicorn main:app --reload 75 | 76 | ``` 77 | 78 | ## Environment 79 | 80 | Make sure to update the environment variables in **ecommerce/config.py**, before starting up the project. 81 | 82 | 83 | ![config-file](./misc/images/env_file.png) 84 | 85 | 86 | 87 | ## Celery 88 | 89 | Make sure before starting up Celery, redis is up and running. 90 | 91 | Command to start celery worker : 92 | 93 | ```bash 94 | $ celery -A main.celery worker -l info 95 | ``` 96 | or with execution pool 97 | ```bash 98 | $ celery -A main.celery worker -l info --pool=prefork 99 | ``` 100 | 101 | Reference Materials: 102 | * [Celery Execution Pools: What is it all about?](https://www.distributedpython.com/2018/10/26/celery-execution-pool/) 103 | * [A complete guide to production-ready Celery configuration](https://medium.com/koko-networks/a-complete-guide-to-production-ready-celery-configuration-5777780b3166) 104 | * [Eliminating Task Processing Outages by Replacing RabbitMQ with Apache Kafka Without Downtime](https://doordash.engineering/2020/09/03/eliminating-task-processing-outages-with-kafka/) 105 | 106 | 107 | ![celery-task](./misc/images/celery-task.png) 108 | 109 | ## Testing 110 | 111 | Before proceeding make sure you have created a test database in Postgres. 112 | 113 | ![python-testing](./misc/images/testing.gif) 114 | 115 | 116 | ## DockerHub 117 | - [https://hub.docker.com/r/mukulmantosh/ecommerce-fastapi](https://hub.docker.com/r/mukulmantosh/ecommerce-fastapi) 118 | 119 | 120 | ## References 121 | 122 | If you are interested to know more about AWS with Python, then you can follow the below links. 123 | 124 | - [Developing Serverless APIs using AWS Toolkit](https://www.jetbrains.com/pycharm/guide/tutorials/intro-aws/) 125 | - [Developing Django Application using AWS NICE DCV, high-performance remote desktop and application streaming](https://www.jetbrains.com/pycharm/guide/tutorials/django-aws/) 126 | -------------------------------------------------------------------------------- /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 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 15 | # within the migration file as well as the filename. 16 | # string value is passed to dateutil.tz.gettz() 17 | # leave blank for localtime 18 | # timezone = 19 | 20 | # max length of characters to apply to the 21 | # "slug" field 22 | # truncate_slug_length = 40 23 | 24 | # set to 'true' to run the environment during 25 | # the 'revision' command, regardless of autogenerate 26 | # revision_environment = false 27 | 28 | # set to 'true' to allow .pyc and .pyo files without 29 | # a source .py file to be detected as revisions in the 30 | # versions/ directory 31 | # sourceless = false 32 | 33 | # version location specification; this defaults 34 | # to alembic/versions. When using multiple version 35 | # directories, initial revisions must be specified with --version-path 36 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions 37 | 38 | # the output encoding used when revision files 39 | # are written from script.py.mako 40 | # output_encoding = utf-8 41 | 42 | sqlalchemy.url = driver://user:pass@localhost/dbname 43 | 44 | 45 | [post_write_hooks] 46 | # post_write_hooks defines scripts or Python functions that are run 47 | # on newly generated revision scripts. See the documentation for further 48 | # detail and examples 49 | 50 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 51 | # hooks = black 52 | # black.type = console_scripts 53 | # black.entrypoint = black 54 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 55 | 56 | # Logging configuration 57 | [loggers] 58 | keys = root,sqlalchemy,alembic 59 | 60 | [handlers] 61 | keys = console 62 | 63 | [formatters] 64 | keys = generic 65 | 66 | [logger_root] 67 | level = WARN 68 | handlers = console 69 | qualname = 70 | 71 | [logger_sqlalchemy] 72 | level = WARN 73 | handlers = 74 | qualname = sqlalchemy.engine 75 | 76 | [logger_alembic] 77 | level = INFO 78 | handlers = 79 | qualname = alembic 80 | 81 | [handler_console] 82 | class = StreamHandler 83 | args = (sys.stderr,) 84 | level = NOTSET 85 | formatter = generic 86 | 87 | [formatter_generic] 88 | format = %(levelname)-5.5s [%(name)s] %(message)s 89 | datefmt = %H:%M:%S 90 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | from logging.config import fileConfig 4 | 5 | from alembic import context 6 | from sqlalchemy import engine_from_config, pool 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | fileConfig(config.config_file_name) 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | # target_metadata = None 21 | 22 | from ecommerce import config as config_env 23 | from ecommerce.db import Base # noqa 24 | from ecommerce.user.models import User # noqa 25 | from ecommerce.products.models import Category, Product # noqa 26 | from ecommerce.orders.models import Order, OrderDetails # noqa 27 | from ecommerce.cart.models import Cart, CartItems # noqa 28 | 29 | target_metadata = Base.metadata 30 | 31 | 32 | # other values from the config, defined by the needs of env.py, 33 | # can be acquired: 34 | # my_important_option = config.get_main_option("my_important_option") 35 | # ... etc. 36 | 37 | 38 | def get_url(): 39 | db_user = config_env.DATABASE_USERNAME 40 | db_password = config_env.DATABASE_PASSWORD 41 | db_host = config_env.DATABASE_HOST 42 | db_name = config_env.DATABASE_NAME 43 | return f"postgresql://{db_user}:{db_password}@{db_host}/{db_name}" 44 | 45 | 46 | def run_migrations_offline(): 47 | """Run migrations in 'offline' mode. 48 | 49 | This configures the context with just a URL 50 | and not an Engine, though an Engine is acceptable 51 | here as well. By skipping the Engine creation 52 | we don't even need a DBAPI to be available. 53 | 54 | Calls to context.execute() here emit the given string to the 55 | script output. 56 | 57 | """ 58 | url = get_url() 59 | context.configure( 60 | url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True 61 | ) 62 | 63 | with context.begin_transaction(): 64 | context.run_migrations() 65 | 66 | 67 | def run_migrations_online(): 68 | """Run migrations in 'online' mode. 69 | 70 | In this scenario we need to create an Engine 71 | and associate a connection with the context. 72 | 73 | """ 74 | configuration = config.get_section(config.config_ini_section) 75 | configuration["sqlalchemy.url"] = get_url() 76 | connectable = engine_from_config( 77 | configuration, prefix="sqlalchemy.", poolclass=pool.NullPool, 78 | ) 79 | 80 | with connectable.connect() as connection: 81 | context.configure( 82 | connection=connection, target_metadata=target_metadata, compare_type=True 83 | ) 84 | 85 | with context.begin_transaction(): 86 | context.run_migrations() 87 | 88 | 89 | if context.is_offline_mode(): 90 | run_migrations_offline() 91 | else: 92 | run_migrations_online() 93 | -------------------------------------------------------------------------------- /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(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /alembic/versions/38a01968532b_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 38a01968532b 4 | Revises: 3ecb256a8f85 5 | Create Date: 2021-08-18 05:16:49.435273 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '38a01968532b' 14 | down_revision = '3ecb256a8f85' 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('order', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('order_date', sa.DateTime(), nullable=True), 24 | sa.Column('order_amount', sa.Float(), nullable=True), 25 | sa.Column('order_status', sa.String(), nullable=True), 26 | sa.Column('shipping_address', sa.Text(), nullable=True), 27 | sa.Column('customer_id', sa.Integer(), nullable=True), 28 | sa.ForeignKeyConstraint(['customer_id'], ['users.id'], ondelete='CASCADE'), 29 | sa.PrimaryKeyConstraint('id') 30 | ) 31 | op.create_table('order_details', 32 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 33 | sa.Column('order_id', sa.Integer(), nullable=True), 34 | sa.Column('product_id', sa.Integer(), nullable=True), 35 | sa.Column('quantity', sa.Integer(), nullable=True), 36 | sa.Column('created', sa.DateTime(), nullable=True), 37 | sa.ForeignKeyConstraint(['order_id'], ['order.id'], ondelete='CASCADE'), 38 | sa.ForeignKeyConstraint(['product_id'], ['products.id'], ondelete='CASCADE'), 39 | sa.PrimaryKeyConstraint('id') 40 | ) 41 | # ### end Alembic commands ### 42 | 43 | 44 | def downgrade(): 45 | # ### commands auto generated by Alembic - please adjust! ### 46 | op.drop_table('order_details') 47 | op.drop_table('order') 48 | # ### end Alembic commands ### 49 | -------------------------------------------------------------------------------- /alembic/versions/3ecb256a8f85_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 3ecb256a8f85 4 | Revises: 5 | Create Date: 2021-08-11 05:40:43.656315 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '3ecb256a8f85' 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('category', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('name', sa.String(length=50), nullable=True), 24 | sa.PrimaryKeyConstraint('id') 25 | ) 26 | op.create_table('users', 27 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 28 | sa.Column('name', sa.String(length=50), nullable=True), 29 | sa.Column('email', sa.String(length=255), nullable=True), 30 | sa.Column('password', sa.String(length=255), nullable=True), 31 | sa.PrimaryKeyConstraint('id'), 32 | sa.UniqueConstraint('email') 33 | ) 34 | op.create_table('products', 35 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 36 | sa.Column('name', sa.String(length=50), nullable=True), 37 | sa.Column('quantity', sa.Integer(), nullable=True), 38 | sa.Column('description', sa.Text(), nullable=True), 39 | sa.Column('price', sa.Float(), nullable=True), 40 | sa.Column('category_id', sa.Integer(), nullable=True), 41 | sa.ForeignKeyConstraint(['category_id'], ['category.id'], ondelete='CASCADE'), 42 | sa.PrimaryKeyConstraint('id') 43 | ) 44 | # ### end Alembic commands ### 45 | 46 | 47 | def downgrade(): 48 | # ### commands auto generated by Alembic - please adjust! ### 49 | op.drop_table('products') 50 | op.drop_table('users') 51 | op.drop_table('category') 52 | # ### end Alembic commands ### 53 | -------------------------------------------------------------------------------- /alembic/versions/b04eb3b09276_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: b04eb3b09276 4 | Revises: 38a01968532b 5 | Create Date: 2021-08-18 05:29:47.408614 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'b04eb3b09276' 14 | down_revision = '38a01968532b' 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('cart', 22 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 23 | sa.Column('user_id', sa.Integer(), nullable=True), 24 | sa.Column('created_date', sa.DateTime(), nullable=True), 25 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), 26 | sa.PrimaryKeyConstraint('id') 27 | ) 28 | op.create_table('cart_items', 29 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 30 | sa.Column('cart_id', sa.Integer(), nullable=True), 31 | sa.Column('product_id', sa.Integer(), nullable=True), 32 | sa.Column('created_date', sa.DateTime(), nullable=True), 33 | sa.ForeignKeyConstraint(['cart_id'], ['cart.id'], ondelete='CASCADE'), 34 | sa.ForeignKeyConstraint(['product_id'], ['products.id'], ondelete='CASCADE'), 35 | sa.PrimaryKeyConstraint('id') 36 | ) 37 | # ### end Alembic commands ### 38 | 39 | 40 | def downgrade(): 41 | # ### commands auto generated by Alembic - please adjust! ### 42 | op.drop_table('cart_items') 43 | op.drop_table('cart') 44 | # ### end Alembic commands ### 45 | -------------------------------------------------------------------------------- /charts/fastapi-helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/fastapi-helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: fastapi-helm 3 | description: A Helm chart for Kubernetes & FastAPI 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.0.0" 25 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Welcome to FastAPI with Kubernetes Helm Chart 2 | 3 | 1. Get the application URL by running these commands: 4 | 5 | * NOTE: It may take a few minutes for all the services to be available. 6 | 7 | You can watch the status of by running 'kubectl get all --namespace {{ .Release.Namespace }}' 8 | 9 | 10 | export NODE_PORT=$(kubectl get svc nginx-service-{{ include "fastapi-helm.fullname" . }} -o jsonpath="{.spec.ports[0].nodePort}" --namespace {{ .Release.Namespace }}) 11 | 12 | 13 | echo "Visit http://127.0.0.1:$NODE_PORT to use your application" 14 | 15 | echo "Happy Helming with FastAPI :)" 16 | 17 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "fastapi-helm.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "fastapi-helm.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "fastapi-helm.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "fastapi-helm.labels" -}} 37 | helm.sh/chart: {{ include "fastapi-helm.chart" . }} 38 | {{ include "fastapi-helm.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "fastapi-helm.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "fastapi-helm.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | 54 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/celery/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: celery-deployment-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | version: {{.Chart.AppVersion}} 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: celery-app 14 | template: 15 | metadata: 16 | labels: 17 | app: celery-app 18 | spec: 19 | initContainers: 20 | - name: init-redis-service 21 | image: busybox:1.28 22 | command: [ 'sh', '-c', "until nslookup redis-service.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for redis-service; sleep 2; done" ] 23 | 24 | containers: 25 | - image: 26 | command: ['celery', '-A', 'main.celery', 'worker', '-l', 'info'] 27 | envFrom: 28 | - secretRef: 29 | name: celery-secret-{{ include "fastapi-helm.fullname" . }} 30 | name: celery-container 31 | 32 | 33 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/celery/secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: celery-secret-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | version: {{.Chart.AppVersion}} 9 | data: 10 | REDIS_HOST: cmVkaXMtc2VydmljZQo= 11 | REDIS_PORT: NjM3OQo= 12 | REDIS_DB: MAo= 13 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/code/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: ecommerce-deployment-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | version: {{ .Chart.AppVersion}} 9 | spec: 10 | replicas: 8 11 | selector: 12 | matchLabels: 13 | app: ecommerce-app 14 | template: 15 | metadata: 16 | labels: 17 | app: ecommerce-app 18 | spec: 19 | initContainers: 20 | - name: init-postgres-service 21 | image: postgres:10.17 22 | command: ['sh', '-c', 23 | 'until pg_isready -h postgres-service.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local -p 5432; 24 | do echo waiting for database; sleep 2; done;'] 25 | containers: 26 | - image: 27 | imagePullPolicy: Always 28 | name: sample-container 29 | envFrom: 30 | - secretRef: 31 | name: ecommerce-secret-{{ include "fastapi-helm.fullname" . }} 32 | ports: 33 | - containerPort: 5000 34 | name: fastapi 35 | readinessProbe: 36 | httpGet: 37 | port: 5000 38 | path: /docs 39 | initialDelaySeconds: 15 40 | livenessProbe: 41 | httpGet: 42 | port: 5000 43 | path: /docs 44 | initialDelaySeconds: 15 45 | periodSeconds: 15 46 | resources: 47 | requests: 48 | memory: "512Mi" 49 | cpu: "0.5" 50 | limits: 51 | memory: "1Gi" 52 | cpu: "1" 53 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/code/secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: ecommerce-secret-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | version: {{ .Chart.AppVersion }} 9 | data: 10 | DATABASE_USERNAME: bXVrdWxtYW50b3No 11 | DATABASE_PASSWORD: bXVrdWwxMjM= 12 | DATABASE_HOST: cG9zdGdyZXMtc2VydmljZQ== 13 | DATABASE_NAME: c2FtcGxlZGI= -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/code/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: ecommerce-service 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | spec: 9 | selector: 10 | app: ecommerce-app 11 | ports: 12 | - port: 5000 13 | targetPort: 5000 14 | 15 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/job/migration.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: fastapi-migrations-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce-migration 8 | version: {{.Chart.AppVersion}} 9 | spec: 10 | ttlSecondsAfterFinished: 100 11 | template: 12 | spec: 13 | containers: 14 | - name: migration-container 15 | image: 16 | command: ['alembic', 'upgrade', 'head'] 17 | envFrom: 18 | - secretRef: 19 | name: migration-secret-{{ include "fastapi-helm.fullname" . }} 20 | initContainers: 21 | - name: init-postgres-service 22 | image: postgres:10.17 23 | command: ['sh', '-c', 24 | 'until pg_isready -h postgres-service.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local -p 5432; 25 | do echo waiting for database; sleep 2; done;'] 26 | restartPolicy: OnFailure 27 | backoffLimit: 15 -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/job/secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: migration-secret-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce-migration 8 | version: {{.Chart.AppVersion}} 9 | data: 10 | DATABASE_USERNAME: bXVrdWxtYW50b3No 11 | DATABASE_PASSWORD: bXVrdWwxMjM= 12 | DATABASE_HOST: cG9zdGdyZXMtc2VydmljZQ== 13 | DATABASE_NAME: c2FtcGxlZGI= -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/nginx/nginx-config.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: nginx-config-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | version: {{ .Chart.AppVersion}} 9 | data: 10 | default.conf: | 11 | upstream ecommerce_project { 12 | server ecommerce-service:5000; 13 | } 14 | server { 15 | 16 | listen 80; 17 | 18 | location / { 19 | proxy_pass http://ecommerce_project; 20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | proxy_set_header Host $host; 22 | proxy_redirect off; 23 | } 24 | } -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/nginx/nginx-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | version: {{ .Chart.AppVersion}} 9 | spec: 10 | replicas: 8 11 | selector: 12 | matchLabels: 13 | app: ecommerce-nginx 14 | template: 15 | metadata: 16 | labels: 17 | app: ecommerce-nginx 18 | spec: 19 | containers: 20 | - image: "{{ .Values.nginxImage.repository }}:{{ .Values.nginxImage.tag | default .Chart.AppVersion }}" 21 | imagePullPolicy: {{ .Values.nginxImage.pullPolicy }} 22 | name: nginx-container 23 | ports: 24 | - containerPort: 80 25 | readinessProbe: 26 | httpGet: 27 | port: 80 28 | path: /docs 29 | initialDelaySeconds: 15 30 | livenessProbe: 31 | httpGet: 32 | port: 80 33 | path: /docs 34 | initialDelaySeconds: 15 35 | periodSeconds: 15 36 | volumeMounts: 37 | - name: nginx-config 38 | mountPath: /etc/nginx/conf.d/default.conf 39 | subPath: default.conf 40 | volumes: 41 | - name: nginx-config 42 | configMap: 43 | name: nginx-config-{{ include "fastapi-helm.fullname" . }} 44 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/nginx/nginx-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: nginx-service-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | version: {{ .Chart.AppVersion}} 9 | spec: 10 | type: NodePort 11 | selector: 12 | app: ecommerce-nginx 13 | ports: 14 | - port: 80 15 | targetPort: 80 16 | 17 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/postgres/postgres-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: postgres-deployment-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | version: {{ .Chart.AppVersion }} 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: postgres-container 13 | template: 14 | metadata: 15 | labels: 16 | app: postgres-container 17 | tier: backend 18 | spec: 19 | containers: 20 | - name: postgres-container 21 | image: postgres:10.17 22 | envFrom: 23 | - secretRef: 24 | name: postgres-secret-{{ include "fastapi-helm.fullname" . }} 25 | ports: 26 | - containerPort: 5432 27 | resources: 28 | requests: 29 | memory: "512Mi" 30 | cpu: "0.5" 31 | limits: 32 | memory: "1Gi" 33 | cpu: "1" 34 | volumeMounts: 35 | - name: postgres-volume-mount 36 | mountPath: /var/lib/postgresql/data 37 | volumes: 38 | - name: postgres-volume-mount 39 | persistentVolumeClaim: 40 | claimName: postgres-pvc -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/postgres/postgres-pv.yml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolume 2 | apiVersion: v1 3 | metadata: 4 | name: postgres-pv 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | type: local 8 | app: ecommerce 9 | version: {{.Chart.AppVersion}} 10 | spec: 11 | persistentVolumeReclaimPolicy: Delete 12 | storageClassName: local-storage 13 | capacity: 14 | storage: 2Gi 15 | volumeMode: Filesystem 16 | accessModes: 17 | - ReadWriteMany 18 | local: 19 | path: /run/desktop/mnt/host/e/postgres-data # <-- if running with docker desktop in windows 20 | nodeAffinity: 21 | required: 22 | nodeSelectorTerms: 23 | - matchExpressions: 24 | - key: kubernetes.io/hostname 25 | operator: In 26 | values: 27 | - docker-desktop # <-- name of the node -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/postgres/postgres-pvc.yml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: postgres-pvc 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | type: local 8 | app: ecommerce 9 | version: {{.Chart.AppVersion}} 10 | spec: 11 | storageClassName: local-storage 12 | accessModes: 13 | - ReadWriteMany 14 | resources: 15 | requests: 16 | storage: 2Gi 17 | volumeName: postgres-pv -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/postgres/postgres-secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: postgres-secret-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | version: {{.Chart.AppVersion}} 9 | data: 10 | POSTGRES_DB: c2FtcGxlZGI= 11 | POSTGRES_USER: bXVrdWxtYW50b3No 12 | POSTGRES_PASSWORD: bXVrdWwxMjM= 13 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/postgres/postgres-service.yml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: postgres-service 5 | namespace: {{ .Release.Namespace }} 6 | spec: 7 | selector: 8 | app: postgres-container 9 | ports: 10 | - protocol: TCP 11 | port: 5432 12 | targetPort: 5432 -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/redis/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: redis-deployment-{{ include "fastapi-helm.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | version: {{ .Chart.AppVersion }} 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: redis-app 14 | template: 15 | metadata: 16 | labels: 17 | app: redis-app 18 | spec: 19 | containers: 20 | - image: redis:6.2.5-alpine 21 | imagePullPolicy: IfNotPresent 22 | name: redis-container 23 | 24 | 25 | -------------------------------------------------------------------------------- /charts/fastapi-helm/templates/redis/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: redis-service 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app: ecommerce 8 | version: {{ .Chart.AppVersion }} 9 | spec: 10 | selector: 11 | app: redis-app 12 | ports: 13 | - port: 6379 14 | targetPort: 6379 15 | 16 | -------------------------------------------------------------------------------- /charts/fastapi-helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for fastapi-helm. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | nginxImage: 8 | repository: nginx 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "1.21" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | podAnnotations: {} 27 | 28 | podSecurityContext: {} 29 | # fsGroup: 2000 30 | 31 | securityContext: {} 32 | # capabilities: 33 | # drop: 34 | # - ALL 35 | # readOnlyRootFilesystem: true 36 | # runAsNonRoot: true 37 | # runAsUser: 1000 38 | 39 | service: 40 | type: ClusterIP 41 | port: 80 42 | 43 | ingress: 44 | enabled: false 45 | className: "" 46 | annotations: {} 47 | # kubernetes.io/ingress.class: nginx 48 | # kubernetes.io/tls-acme: "true" 49 | hosts: 50 | - host: chart-example.local 51 | paths: 52 | - path: / 53 | pathType: ImplementationSpecific 54 | tls: [] 55 | # - secretName: chart-example-tls 56 | # hosts: 57 | # - chart-example.local 58 | 59 | resources: {} 60 | # We usually recommend not to specify default resources and to leave this as a conscious 61 | # choice for the user. This also increases chances charts run on environments with little 62 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 63 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 64 | # limits: 65 | # cpu: 100m 66 | # memory: 128Mi 67 | # requests: 68 | # cpu: 100m 69 | # memory: 128Mi 70 | 71 | 72 | -------------------------------------------------------------------------------- /conf_test_db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker 3 | 4 | from ecommerce import config 5 | from ecommerce.db import Base, get_db 6 | from main import app 7 | 8 | DATABASE_USERNAME = config.DATABASE_USERNAME 9 | DATABASE_PASSWORD = config.DATABASE_PASSWORD 10 | DATABASE_HOST = config.DATABASE_HOST 11 | DATABASE_NAME = config.TEST_DATABASE_NAME 12 | 13 | SQLALCHEMY_DATABASE_URL = f"postgresql://{DATABASE_USERNAME}:{DATABASE_PASSWORD}@{DATABASE_HOST}/{DATABASE_NAME}" 14 | 15 | engine = create_engine(SQLALCHEMY_DATABASE_URL) 16 | TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 17 | 18 | Base.metadata.drop_all(bind=engine) 19 | Base.metadata.create_all(bind=engine) 20 | 21 | 22 | def override_get_db(): 23 | try: 24 | db = TestingSessionLocal() 25 | yield db 26 | finally: 27 | db.close() 28 | 29 | 30 | app.dependency_overrides[get_db] = override_get_db 31 | -------------------------------------------------------------------------------- /ecommerce/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/ecommerce/__init__.py -------------------------------------------------------------------------------- /ecommerce/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/ecommerce/auth/__init__.py -------------------------------------------------------------------------------- /ecommerce/auth/jwt.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from fastapi import Depends, HTTPException, status 4 | from fastapi.security import OAuth2PasswordBearer 5 | from jose import JWTError, jwt 6 | 7 | from ecommerce.auth import schema 8 | 9 | SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" 10 | ALGORITHM = "HS256" 11 | ACCESS_TOKEN_EXPIRE_MINUTES = 30 12 | 13 | 14 | def create_access_token(data: dict): 15 | to_encode = data.copy() 16 | expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 17 | to_encode.update({"exp": expire}) 18 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) 19 | return encoded_jwt 20 | 21 | 22 | def verify_token(token: str, credentials_exception): 23 | try: 24 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) 25 | email: str = payload.get("sub") 26 | if email is None: 27 | raise credentials_exception 28 | token_data = schema.TokenData(email=email) 29 | return token_data 30 | except JWTError: 31 | raise credentials_exception 32 | 33 | 34 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") 35 | 36 | 37 | def get_current_user(data: str = Depends(oauth2_scheme)): 38 | credentials_exception = HTTPException( 39 | status_code=status.HTTP_401_UNAUTHORIZED, 40 | detail="Could not validate credentials", 41 | headers={"WWW-Authenticate": "Bearer"}, 42 | ) 43 | return verify_token(data, credentials_exception) 44 | -------------------------------------------------------------------------------- /ecommerce/auth/router.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from fastapi import APIRouter, Depends, HTTPException, status 4 | from fastapi.security import OAuth2PasswordRequestForm 5 | from sqlalchemy.orm import Session 6 | 7 | 8 | from ecommerce import db 9 | from ecommerce.user import hashing 10 | from ecommerce.user.models import User 11 | 12 | from .jwt import create_access_token 13 | 14 | router = APIRouter( 15 | tags=['auth'] 16 | ) 17 | 18 | 19 | @router.post('/login') 20 | def login(request: OAuth2PasswordRequestForm = Depends(), database: Session = Depends(db.get_db)): 21 | user = database.query(User).filter(User.email == request.username).first() 22 | if not user: 23 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Invalid Credentials') 24 | 25 | if not hashing.verify_password(request.password, user.password): 26 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid Password') 27 | 28 | # Generate a JWT Token 29 | access_token = create_access_token(data={"sub": user.email}) 30 | 31 | return {"access_token": access_token, "token_type": "bearer"} 32 | 33 | -------------------------------------------------------------------------------- /ecommerce/auth/schema.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Login(BaseModel): 7 | username: str 8 | password: str 9 | 10 | 11 | class Token(BaseModel): 12 | access_token: str 13 | token_type: str 14 | 15 | 16 | class TokenData(BaseModel): 17 | email: Optional[str] = None 18 | -------------------------------------------------------------------------------- /ecommerce/cart/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/ecommerce/cart/__init__.py -------------------------------------------------------------------------------- /ecommerce/cart/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import Column, Integer, ForeignKey, DateTime 4 | from sqlalchemy.orm import relationship 5 | 6 | from ecommerce.db import Base 7 | from ecommerce.products.models import Product 8 | from ecommerce.user.models import User 9 | 10 | 11 | class Cart(Base): 12 | __tablename__ = "cart" 13 | 14 | id = Column(Integer, primary_key=True, autoincrement=True) 15 | user_id = Column(Integer, ForeignKey(User.id, ondelete="CASCADE"), ) 16 | cart_items = relationship("CartItems", back_populates="cart") 17 | user_cart = relationship("User", back_populates="cart") 18 | created_date = Column(DateTime, default=datetime.now) 19 | 20 | 21 | class CartItems(Base): 22 | __tablename__ = "cart_items" 23 | 24 | id = Column(Integer, primary_key=True, autoincrement=True) 25 | cart_id = Column(Integer, ForeignKey("cart.id", ondelete="CASCADE"), ) 26 | product_id = Column(Integer, ForeignKey(Product.id, ondelete="CASCADE"), ) 27 | cart = relationship("Cart", back_populates="cart_items") 28 | products = relationship("Product", back_populates="cart_items") 29 | created_date = Column(DateTime, default=datetime.now) 30 | -------------------------------------------------------------------------------- /ecommerce/cart/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, status, Response 2 | from sqlalchemy.orm import Session 3 | 4 | from ecommerce import db 5 | from ecommerce.auth.jwt import get_current_user 6 | from ecommerce.user.schema import User 7 | from .schema import ShowCart 8 | from .services import add_to_cart, get_all_items, remove_cart_item 9 | 10 | router = APIRouter( 11 | tags=['Cart'], 12 | prefix='/cart' 13 | ) 14 | 15 | 16 | @router.get('/', response_model=ShowCart) 17 | async def get_all_cart_items(current_user: User = Depends(get_current_user), 18 | database: Session = Depends(db.get_db)): 19 | result = await get_all_items(current_user, database) 20 | return result 21 | 22 | 23 | @router.get('/add', status_code=status.HTTP_201_CREATED) 24 | async def add_product_to_cart(product_id: int, current_user: User = Depends(get_current_user), 25 | database: Session = Depends(db.get_db)): 26 | result = await add_to_cart(product_id, current_user, database) 27 | return result 28 | 29 | 30 | @router.delete('/{cart_item_id}', status_code=status.HTTP_204_NO_CONTENT, response_class=Response) 31 | async def remove_cart_item_by_id(cart_item_id: int, current_user: User = Depends(get_current_user), 32 | database: Session = Depends(db.get_db)): 33 | await remove_cart_item(cart_item_id, current_user, database) 34 | -------------------------------------------------------------------------------- /ecommerce/cart/schema.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import List 3 | 4 | from pydantic import BaseModel 5 | 6 | from ecommerce.products.schema import Product 7 | 8 | 9 | class ShowCartItems(BaseModel): 10 | id: int 11 | products: Product 12 | created_date: datetime.datetime 13 | 14 | class Config: 15 | orm_mode = True 16 | 17 | 18 | class ShowCart(BaseModel): 19 | id: int 20 | cart_items: List[ShowCartItems] = [] 21 | 22 | class Config: 23 | orm_mode = True 24 | -------------------------------------------------------------------------------- /ecommerce/cart/services.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException, status, Depends 2 | from sqlalchemy import and_ 3 | from sqlalchemy.orm import Session 4 | 5 | from ecommerce import db 6 | from ecommerce.products.models import Product 7 | from ecommerce.user.models import User 8 | from .models import Cart, CartItems 9 | from .schema import ShowCart 10 | 11 | 12 | async def add_to_cart(product_id: int, current_user, database: Session = Depends(db.get_db)): 13 | product_info = database.query(Product).get(product_id) 14 | if not product_id: 15 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Data Not Found !") 16 | 17 | if product_info.quantity <= 0: 18 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item Out of Stock !") 19 | 20 | user_info = database.query(User).filter(User.email == current_user.email).first() 21 | 22 | cart_info = database.query(Cart).filter(Cart.user_id == user_info.id).first() 23 | if not cart_info: 24 | new_cart = Cart(user_id=user_info.id) 25 | database.add(new_cart) 26 | database.commit() 27 | database.refresh(new_cart) 28 | await add_items(new_cart.id, product_info.id, database) 29 | else: 30 | await add_items(cart_info.id, product_info.id, database) 31 | return {"status": "Item Added to Cart"} 32 | 33 | 34 | async def add_items(cart_id: int, product_id: int, database: Session = Depends(db.get_db)): 35 | cart_items = CartItems(cart_id=cart_id, product_id=product_id) 36 | database.add(cart_items) 37 | database.commit() 38 | database.refresh(cart_items) 39 | 40 | product_object = database.query(Product).filter(Product.id == product_id) 41 | current_quantity = product_object.first().quantity - 1 42 | product_object.update({"quantity": current_quantity}) 43 | database.commit() 44 | return {'detail': 'Object Updated'} 45 | 46 | 47 | async def get_all_items(current_user, database) -> ShowCart: 48 | user_info = database.query(User).filter(User.email == current_user.email).first() 49 | cart = database.query(Cart).filter(Cart.user_id == user_info.id).first() 50 | return cart 51 | 52 | 53 | async def remove_cart_item(cart_item_id: int, current_user, database) -> None: 54 | user_info = database.query(User).filter(User.email == current_user.email).first() 55 | cart_id = database.query(Cart).filter(User.id == user_info.id).first() 56 | database.query(CartItems).filter(and_(CartItems.id == cart_item_id, CartItems.cart_id == cart_id.id)).delete() 57 | database.commit() 58 | return 59 | -------------------------------------------------------------------------------- /ecommerce/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | APP_ENV = os.getenv('APP_ENV', 'development') 4 | DATABASE_USERNAME = os.getenv('DATABASE_USERNAME', 'postgres') 5 | DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD', 'mukul123') 6 | DATABASE_HOST = os.getenv('DATABASE_HOST', '192.168.0.101') 7 | DATABASE_NAME = os.getenv('DATABASE_NAME', 'mukuldb') 8 | TEST_DATABASE_NAME = os.getenv('DATABASE_NAME', 'test_mukuldb') 9 | REDIS_HOST = os.getenv('REDIS_HOST', '127.0.0.1') 10 | REDIS_PORT = os.getenv('REDIS_PORT', '6379') 11 | REDIS_DB = os.getenv('REDIS_DB', '0' if APP_ENV == 'TESTING' else '0') 12 | -------------------------------------------------------------------------------- /ecommerce/db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | from . import config 6 | 7 | DATABASE_USERNAME = config.DATABASE_USERNAME 8 | DATABASE_PASSWORD = config.DATABASE_PASSWORD 9 | DATABASE_HOST = config.DATABASE_HOST 10 | DATABASE_NAME = config.DATABASE_NAME 11 | 12 | SQLALCHEMY_DATABASE_URL = f"postgresql://{DATABASE_USERNAME}:{DATABASE_PASSWORD}@{DATABASE_HOST}/{DATABASE_NAME}" 13 | 14 | engine = create_engine(SQLALCHEMY_DATABASE_URL) 15 | 16 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 17 | 18 | Base = declarative_base() 19 | 20 | 21 | def get_db(): 22 | db = SessionLocal() 23 | try: 24 | yield db 25 | finally: 26 | db.close() 27 | -------------------------------------------------------------------------------- /ecommerce/orders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/ecommerce/orders/__init__.py -------------------------------------------------------------------------------- /ecommerce/orders/mail.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import boto3 4 | from botocore.exceptions import ClientError 5 | 6 | 7 | def clean_html(raw_html): 8 | cleaner = re.compile('<.*?>') 9 | clean_text = re.sub(cleaner, '', raw_html) 10 | return clean_text 11 | 12 | 13 | # Replace sender@example.com with your "From" address. 14 | # This address must be verified with Amazon SES. 15 | SENDER = "FastAPI " 16 | 17 | # If necessary, replace us-west-2 with the AWS Region you're using for Amazon SES. 18 | AWS_REGION = "ap-south-1" 19 | 20 | # The subject line for the email. 21 | SUBJECT = "New Order Placed" 22 | 23 | # The email body for recipients with non-HTML email clients. 24 | 25 | 26 | # The HTML body of the email. 27 | BODY_HTML = """ 28 | 29 | 30 |

Order Successfully Placed !

31 |

Hi, Your new order has been successfully placed. You will receive more information shortly.

32 | 33 | 34 | """ 35 | 36 | BODY_TEXT = clean_html(BODY_HTML) 37 | 38 | # The character encoding for the email. 39 | CHARSET = "UTF-8" 40 | 41 | # Create a new SES resource and specify a region. 42 | client = boto3.client('ses', region_name=AWS_REGION) 43 | 44 | 45 | def order_notification(recipient): 46 | # Try to send the email. 47 | try: 48 | # Provide the contents of the email. 49 | response = client.send_email( 50 | Destination={ 51 | 'ToAddresses': [ 52 | recipient, 53 | ], 54 | }, 55 | Message={ 56 | 'Body': { 57 | 'Html': { 58 | 'Charset': CHARSET, 59 | 'Data': BODY_HTML, 60 | }, 61 | 'Text': { 62 | 'Charset': CHARSET, 63 | 'Data': BODY_TEXT, 64 | }, 65 | }, 66 | 'Subject': { 67 | 'Charset': CHARSET, 68 | 'Data': SUBJECT, 69 | }, 70 | }, 71 | Source=SENDER, 72 | # If you are not using a configuration set, comment or delete the 73 | # following line 74 | # ConfigurationSetName=CONFIGURATION_SET, 75 | ) 76 | # Display an error if something goes wrong. 77 | except ClientError as e: 78 | print(e.response['Error']['Message']) 79 | else: 80 | print("Email sent! Message ID:"), 81 | print(response['MessageId']) 82 | -------------------------------------------------------------------------------- /ecommerce/orders/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import Column, Integer, String, Float, ForeignKey, Text, DateTime 4 | from sqlalchemy.orm import relationship 5 | from ecommerce.user.models import User 6 | from ecommerce.products.models import Product 7 | from ecommerce.db import Base 8 | 9 | 10 | class Order(Base): 11 | __tablename__ = "order" 12 | 13 | id = Column(Integer, primary_key=True, autoincrement=True) 14 | order_date = Column(DateTime, default=datetime.now) 15 | order_amount = Column(Float, default=0.0) 16 | order_status = Column(String, default="PROCESSING") 17 | shipping_address = Column(Text) 18 | customer_id = Column(Integer, ForeignKey(User.id, ondelete="CASCADE"), ) 19 | order_details = relationship("OrderDetails", back_populates="order") 20 | user_info = relationship("User", back_populates="order") 21 | 22 | 23 | class OrderDetails(Base): 24 | __tablename__ = "order_details" 25 | 26 | id = Column(Integer, primary_key=True, autoincrement=True) 27 | order_id = Column(Integer, ForeignKey('order.id', ondelete="CASCADE"), ) 28 | product_id = Column(Integer, ForeignKey(Product.id, ondelete="CASCADE"), ) 29 | order = relationship("Order", back_populates="order_details") 30 | product_order_details = relationship("Product", back_populates="order_details") 31 | quantity = Column(Integer, default=1) 32 | created = Column(DateTime, default=datetime.now) 33 | -------------------------------------------------------------------------------- /ecommerce/orders/router.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, status 4 | from sqlalchemy.orm import Session 5 | 6 | from ecommerce import db 7 | from ecommerce.auth.jwt import get_current_user 8 | from ecommerce.orders.services import initiate_order, get_order_listing 9 | from ecommerce.user.schema import User 10 | from .schema import ShowOrder 11 | 12 | router = APIRouter( 13 | tags=['Orders'], 14 | prefix='/orders' 15 | ) 16 | 17 | 18 | @router.post('/', status_code=status.HTTP_201_CREATED, response_model=ShowOrder) 19 | async def initiate_order_processing(current_user: User = Depends(get_current_user), 20 | database: Session = Depends(db.get_db)): 21 | result = await initiate_order(current_user, database) 22 | return result 23 | 24 | 25 | @router.get('/', status_code=status.HTTP_200_OK, response_model=List[ShowOrder]) 26 | async def orders_list(current_user: User = Depends(get_current_user), 27 | database: Session = Depends(db.get_db)): 28 | result = await get_order_listing(current_user, database) 29 | return result 30 | -------------------------------------------------------------------------------- /ecommerce/orders/schema.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import List, Optional 3 | 4 | from pydantic import BaseModel 5 | 6 | from ecommerce.products.schema import ProductListing 7 | 8 | 9 | class ShowOrderDetails(BaseModel): 10 | id: int 11 | order_id: int 12 | product_order_details: ProductListing 13 | 14 | class Config: 15 | orm_mode = True 16 | 17 | 18 | class ShowOrder(BaseModel): 19 | id: Optional[int] 20 | order_date: datetime.datetime 21 | order_amount: float 22 | order_status: str 23 | shipping_address: str 24 | order_details: List[ShowOrderDetails] = [] 25 | 26 | class Config: 27 | orm_mode = True 28 | -------------------------------------------------------------------------------- /ecommerce/orders/services.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import HTTPException, status 4 | 5 | from ecommerce.cart.models import Cart, CartItems 6 | from ecommerce.orders.models import Order, OrderDetails 7 | from ecommerce.user.models import User 8 | from . import tasks 9 | 10 | 11 | async def initiate_order(current_user, database) -> Order: 12 | user_info = database.query(User).filter(User.email == current_user.email).first() 13 | cart = database.query(Cart).filter(Cart.user_id == user_info.id).first() 14 | 15 | cart_items_objects = database.query(CartItems).filter(Cart.id == cart.id) 16 | if not cart_items_objects.count(): 17 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No Items found in Cart !") 18 | 19 | total_amount: float = 0.0 20 | for item in cart_items_objects: 21 | total_amount += item.products.price 22 | 23 | new_order = Order(order_amount=total_amount, 24 | shipping_address="587 Hinkle Deegan Lake Road, Syracuse, New York", 25 | customer_id=user_info.id) 26 | database.add(new_order) 27 | database.commit() 28 | database.refresh(new_order) 29 | 30 | bulk_order_details_objects = list() 31 | for item in cart_items_objects: 32 | new_order_details = OrderDetails(order_id=new_order.id, 33 | product_id=item.products.id) 34 | bulk_order_details_objects.append(new_order_details) 35 | 36 | database.bulk_save_objects(bulk_order_details_objects) 37 | database.commit() 38 | 39 | # Send Email 40 | tasks.send_email.delay(current_user.email) 41 | 42 | # clear items in cart 43 | database.query(CartItems).filter(CartItems.cart_id == cart.id).delete() 44 | database.commit() 45 | 46 | return new_order 47 | 48 | 49 | async def get_order_listing(current_user, database) -> List[Order]: 50 | user_info = database.query(User).filter(User.email == current_user.email).first() 51 | orders = database.query(Order).filter(Order.customer_id == user_info.id).all() 52 | return orders 53 | -------------------------------------------------------------------------------- /ecommerce/orders/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import shared_task 2 | from . import mail 3 | 4 | 5 | @shared_task 6 | def send_email(email): 7 | return mail.order_notification(email) 8 | -------------------------------------------------------------------------------- /ecommerce/products/__init__.py: -------------------------------------------------------------------------------- 1 | from ecommerce.cart.models import CartItems -------------------------------------------------------------------------------- /ecommerce/products/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Float, ForeignKey, Text 2 | from sqlalchemy.orm import relationship 3 | 4 | from ecommerce.db import Base 5 | 6 | 7 | class Category(Base): 8 | __tablename__ = "category" 9 | 10 | id = Column(Integer, primary_key=True, autoincrement=True) 11 | name = Column(String(50)) 12 | 13 | product = relationship("Product", back_populates="category") 14 | 15 | 16 | class Product(Base): 17 | __tablename__ = "products" 18 | 19 | id = Column(Integer, primary_key=True, autoincrement=True) 20 | name = Column(String(50)) 21 | quantity = Column(Integer) 22 | description = Column(Text) 23 | price = Column(Float) 24 | category_id = Column(Integer, ForeignKey('category.id', ondelete="CASCADE"), ) 25 | category = relationship("Category", back_populates="product") 26 | order_details = relationship("OrderDetails", back_populates="product_order_details") 27 | cart_items = relationship("CartItems", back_populates="products") -------------------------------------------------------------------------------- /ecommerce/products/router.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, status, Response, HTTPException 4 | from sqlalchemy.orm import Session 5 | 6 | from ecommerce import db 7 | from . import schema 8 | from . import services 9 | from . import validator 10 | 11 | router = APIRouter( 12 | tags=['Products'], 13 | prefix='/products' 14 | ) 15 | 16 | 17 | @router.post('/category', status_code=status.HTTP_201_CREATED) 18 | async def create_category(request: schema.Category, database: Session = Depends(db.get_db)): 19 | new_category = await services.create_new_category(request, database) 20 | return new_category 21 | 22 | 23 | @router.get('/category', response_model=List[schema.ListCategory]) 24 | async def get_all_categories(database: Session = Depends(db.get_db)): 25 | return await services.get_all_categories(database) 26 | 27 | 28 | @router.get('/category/{category_id}', response_model=schema.ListCategory) 29 | async def get_category_by_id(category_id: int, database: Session = Depends(db.get_db)): 30 | return await services.get_category_by_id(category_id, database) 31 | 32 | 33 | @router.delete('/category/{category_id}', status_code=status.HTTP_204_NO_CONTENT, response_class=Response) 34 | async def delete_category_by_id(category_id: int, database: Session = Depends(db.get_db)): 35 | return await services.delete_category_by_id(category_id, database) 36 | 37 | 38 | @router.post('/', status_code=status.HTTP_201_CREATED) 39 | async def create_product(request: schema.Product, database: Session = Depends(db.get_db)): 40 | category = await validator.verify_category_exist(request.category_id, database) 41 | if not category: 42 | raise HTTPException( 43 | status_code=400, 44 | detail="You have provided invalid category id.", 45 | ) 46 | 47 | product = await services.create_new_product(request, database) 48 | return product 49 | 50 | 51 | @router.get('/', response_model=List[schema.ProductListing]) 52 | async def get_all_products(database: Session = Depends(db.get_db)): 53 | return await services.get_all_products(database) 54 | -------------------------------------------------------------------------------- /ecommerce/products/schema.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, constr 4 | 5 | 6 | class Category(BaseModel): 7 | name: constr(min_length=2, max_length=50) 8 | 9 | 10 | class ListCategory(BaseModel): 11 | id: int 12 | name: str 13 | 14 | class Config: 15 | orm_mode = True 16 | 17 | 18 | class ProductBase(BaseModel): 19 | id: Optional[int] 20 | name: str 21 | quantity: int 22 | description: str 23 | price: float 24 | 25 | class Config: 26 | orm_mode = True 27 | 28 | 29 | class Product(ProductBase): 30 | category_id: int 31 | 32 | 33 | class ProductListing(ProductBase): 34 | category: ListCategory 35 | 36 | class Config: 37 | orm_mode = True 38 | -------------------------------------------------------------------------------- /ecommerce/products/services.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import HTTPException, status 4 | 5 | from . import models 6 | 7 | 8 | async def create_new_category(request, database) -> models.Category: 9 | new_category = models.Category(name=request.name) 10 | database.add(new_category) 11 | database.commit() 12 | database.refresh(new_category) 13 | return new_category 14 | 15 | 16 | async def get_all_categories(database) -> List[models.Category]: 17 | categories = database.query(models.Category).all() 18 | return categories 19 | 20 | 21 | async def get_category_by_id(category_id, database) -> models.Category: 22 | category_info = database.query(models.Category).get(category_id) 23 | if not category_info: 24 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Data Not Found !") 25 | return category_info 26 | 27 | 28 | async def delete_category_by_id(category_id, database): 29 | database.query(models.Category).filter(models.Category.id == category_id).delete() 30 | database.commit() 31 | 32 | 33 | async def create_new_product(request, database) -> models.Product: 34 | new_product = models.Product(name=request.name, quantity=request.quantity, 35 | description=request.description, price=request.price, 36 | category_id=request.category_id) 37 | database.add(new_product) 38 | database.commit() 39 | database.refresh(new_product) 40 | return new_product 41 | 42 | 43 | async def get_all_products(database) -> List[models.Product]: 44 | products = database.query(models.Product).all() 45 | return products 46 | -------------------------------------------------------------------------------- /ecommerce/products/validator.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from .models import Category 6 | 7 | 8 | async def verify_category_exist(category_id: int, db_session: Session) -> Optional[Category]: 9 | return db_session.query(Category).filter(Category.id == category_id).first() 10 | -------------------------------------------------------------------------------- /ecommerce/user/__init__.py: -------------------------------------------------------------------------------- 1 | from ecommerce.orders.models import Order 2 | from ecommerce.cart.models import Cart -------------------------------------------------------------------------------- /ecommerce/user/hashing.py: -------------------------------------------------------------------------------- 1 | from passlib.context import CryptContext 2 | 3 | pwd_context = CryptContext(schemes=["argon2"], deprecated="auto") 4 | 5 | 6 | def verify_password(plain_password, hashed_password): 7 | return pwd_context.verify(plain_password, hashed_password) 8 | 9 | 10 | def get_password_hash(password): 11 | return pwd_context.hash(password) 12 | -------------------------------------------------------------------------------- /ecommerce/user/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | from sqlalchemy.orm import relationship 3 | from ecommerce.db import Base 4 | from . import hashing 5 | 6 | 7 | class User(Base): 8 | __tablename__ = "users" 9 | 10 | id = Column(Integer, primary_key=True, autoincrement=True) 11 | name = Column(String(50)) 12 | email = Column(String(255), unique=True) 13 | password = Column(String(255)) 14 | order = relationship("Order", back_populates="user_info") 15 | cart = relationship("Cart", back_populates="user_cart") 16 | 17 | def __init__(self, name, email, password, *args, **kwargs): 18 | self.name = name 19 | self.email = email 20 | self.password = hashing.get_password_hash(password) 21 | 22 | def check_password(self, password): 23 | return hashing.verify_password(self.password, password) 24 | 25 | -------------------------------------------------------------------------------- /ecommerce/user/router.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi import APIRouter, Depends, status, Response, HTTPException 4 | from sqlalchemy.orm import Session 5 | 6 | from ecommerce import db 7 | from ecommerce.auth.jwt import get_current_user 8 | from . import schema 9 | from . import services 10 | from . import validator 11 | 12 | router = APIRouter( 13 | tags=['Users'], 14 | prefix='/user' 15 | ) 16 | 17 | 18 | # Interesting Question for Global Dependency 19 | # https://github.com/tiangolo/fastapi/issues/2481 20 | 21 | 22 | @router.post('/', status_code=status.HTTP_201_CREATED) 23 | async def create_user_registration(request: schema.User, database: Session = Depends(db.get_db)): 24 | # Read More : Pydantic Validation with Database (https://github.com/tiangolo/fastapi/issues/979) 25 | 26 | user = await validator.verify_email_exist(request.email, database) 27 | if user: 28 | raise HTTPException( 29 | status_code=400, 30 | detail="The user with this email already exists in the system.", 31 | ) 32 | 33 | new_user = await services.new_user_register(request, database) 34 | return new_user 35 | 36 | 37 | @router.get('/', response_model=List[schema.DisplayUser]) 38 | async def get_all_users(database: Session = Depends(db.get_db), current_user: schema.User = Depends(get_current_user)): 39 | return await services.all_users(database) 40 | 41 | 42 | @router.get('/{user_id}', response_model=schema.DisplayUser) 43 | async def get_user_by_id(user_id: int, database: Session = Depends(db.get_db), 44 | current_user: schema.User = Depends(get_current_user)): 45 | return await services.get_user_by_id(user_id, database) 46 | 47 | 48 | @router.delete('/{user_id}', status_code=status.HTTP_204_NO_CONTENT, response_class=Response) 49 | async def delete_user_by_id(user_id: int, database: Session = Depends(db.get_db), 50 | current_user: schema.User = Depends(get_current_user)): 51 | return await services.delete_user_by_id(user_id, database) 52 | -------------------------------------------------------------------------------- /ecommerce/user/schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, constr, validator, EmailStr 2 | 3 | from ecommerce import db 4 | from . import models 5 | 6 | 7 | class User(BaseModel): 8 | name: constr(min_length=2, max_length=50) 9 | email: EmailStr 10 | password: str 11 | 12 | 13 | class DisplayUser(BaseModel): 14 | id: int 15 | name: str 16 | email: str 17 | 18 | class Config: 19 | orm_mode = True 20 | -------------------------------------------------------------------------------- /ecommerce/user/services.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from fastapi import HTTPException, status 4 | 5 | from . import models 6 | 7 | 8 | async def new_user_register(request, database) -> models.User: 9 | new_user = models.User(name=request.name, email=request.email, password=request.password) 10 | database.add(new_user) 11 | database.commit() 12 | database.refresh(new_user) 13 | return new_user 14 | 15 | 16 | async def all_users(database) -> List[models.User]: 17 | users = database.query(models.User).all() 18 | return users 19 | 20 | 21 | async def get_user_by_id(user_id, database) -> Optional[models.User]: 22 | user_info = database.query(models.User).get(user_id) 23 | if not user_info: 24 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Data Not Found !") 25 | return user_info 26 | 27 | 28 | async def delete_user_by_id(user_id, database): 29 | database.query(models.User).filter(models.User.id == user_id).delete() 30 | database.commit() 31 | -------------------------------------------------------------------------------- /ecommerce/user/validator.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from .models import User 6 | 7 | 8 | async def verify_email_exist(email: str, db_session: Session) -> Optional[User]: 9 | return db_session.query(User).filter(User.email == email).first() 10 | -------------------------------------------------------------------------------- /eks/cluster.yml: -------------------------------------------------------------------------------- 1 | # If a nodegroup includes the attachPolicyARNs it must also include the default node policies, 2 | # like AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy and AmazonEC2ContainerRegistryReadOnly. 3 | 4 | apiVersion: eksctl.io/v1alpha5 5 | kind: ClusterConfig 6 | metadata: 7 | name: fastapi-demo 8 | region: ap-south-1 9 | version: "1.21" 10 | managedNodeGroups: 11 | - name: fastapi-private-ng 12 | instanceType: t3a.small 13 | desiredCapacity: 3 14 | minSize: 3 15 | maxSize: 6 16 | volumeType: gp3 17 | volumeSize: 20 18 | privateNetworking: true 19 | iam: 20 | attachPolicyARNs: # Reference : https://eksctl.io/usage/iam-policies/ 21 | - arn:aws:iam::XXXXXXXXXXXXXX:policy/SES_EKS_Policy # <-- this is a custom policy, you need to replace this. 22 | - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy 23 | - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly 24 | - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy 25 | withAddonPolicies: 26 | autoScaler: true 27 | imageBuilder: true 28 | albIngress: true 29 | externalDNS: true 30 | certManager: true 31 | ssh: 32 | allow: true 33 | publicKeyName: "fastapi-demo" # <-- ssh key name, make sure you define the Key Pair before setting up the cluster. Reference : https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html -------------------------------------------------------------------------------- /eks/deploy/celery/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: celery-deployment 5 | labels: 6 | app: ecommerce 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: celery-app 12 | template: 13 | metadata: 14 | labels: 15 | app: celery-app 16 | spec: 17 | containers: 18 | - image: 19 | command: ['celery', '-A', 'main.celery', 'worker', '-l', 'info'] 20 | envFrom: 21 | - secretRef: 22 | name: celery-secret 23 | name: celery-container 24 | 25 | -------------------------------------------------------------------------------- /eks/deploy/celery/secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: celery-secret 5 | labels: 6 | app: ecommerce 7 | data: 8 | REDIS_HOST: cmVkaXMtc2VydmljZQ== 9 | REDIS_PORT: NjM3OQ== 10 | REDIS_DB: MA== 11 | -------------------------------------------------------------------------------- /eks/deploy/code/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: ecommerce-deployment 5 | labels: 6 | app: ecommerce 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: ecommerce-app 12 | template: 13 | metadata: 14 | labels: 15 | app: ecommerce-app 16 | spec: 17 | containers: 18 | - image: 19 | imagePullPolicy: Always 20 | name: sample-container 21 | envFrom: 22 | - secretRef: 23 | name: ecommerce-secret 24 | ports: 25 | - containerPort: 5000 26 | name: fastapi 27 | readinessProbe: 28 | httpGet: 29 | port: 5000 30 | path: /docs 31 | initialDelaySeconds: 15 32 | livenessProbe: 33 | httpGet: 34 | port: 5000 35 | path: /docs 36 | initialDelaySeconds: 15 37 | periodSeconds: 15 38 | -------------------------------------------------------------------------------- /eks/deploy/code/secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: ecommerce-secret 5 | labels: 6 | app: ecommerce 7 | data: 8 | DATABASE_USERNAME: cG9zdGdyZXM= 9 | DATABASE_PASSWORD: bXVrdWwxMjM= 10 | DATABASE_HOST: cG9zdGdyZXMtc2VydmljZQ== 11 | DATABASE_NAME: c2FtcGxl 12 | REDIS_HOST: cmVkaXMtc2VydmljZQ== 13 | REDIS_PORT: NjM3OQ== 14 | REDIS_DB: MA== 15 | -------------------------------------------------------------------------------- /eks/deploy/code/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: ecommerce-service 5 | labels: 6 | app: ecommerce 7 | spec: 8 | type: NodePort 9 | selector: 10 | app: ecommerce-app 11 | ports: 12 | - port: 5000 13 | targetPort: 5000 14 | 15 | -------------------------------------------------------------------------------- /eks/deploy/elasticache/redis-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: redis-service 5 | spec: 6 | type: ExternalName 7 | externalName: 8 | 9 | -------------------------------------------------------------------------------- /eks/deploy/ingress/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: ingress-ecommerce-service 5 | labels: 6 | app: ecommerce 7 | annotations: 8 | kubernetes.io/ingress.class: "alb" 9 | alb.ingress.kubernetes.io/scheme: internet-facing 10 | alb.ingress.kubernetes.io/healthcheck-protocol: HTTP 11 | alb.ingress.kubernetes.io/healthcheck-port: traffic-port 12 | alb.ingress.kubernetes.io/healthcheck-interval-seconds: '15' 13 | alb.ingress.kubernetes.io/healthcheck-timeout-seconds: '5' 14 | alb.ingress.kubernetes.io/success-codes: '200' 15 | alb.ingress.kubernetes.io/healthy-threshold-count: '2' 16 | alb.ingress.kubernetes.io/unhealthy-threshold-count: '2' 17 | alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]' 18 | alb.ingress.kubernetes.io/certificate-arn: 19 | alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS-1-2-Ext-2018-06 20 | alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}' 21 | spec: 22 | rules: 23 | - http: 24 | paths: 25 | - path: /* 26 | backend: 27 | serviceName: ssl-redirect 28 | servicePort: use-annotation 29 | - path: /* 30 | backend: 31 | serviceName: ecommerce-service 32 | servicePort: 5000 33 | -------------------------------------------------------------------------------- /eks/deploy/job/migration.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: fastapi-migrations 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: migration-container 10 | image: 11 | command: ['alembic', 'upgrade', 'head'] 12 | envFrom: 13 | - secretRef: 14 | name: ecommerce-secret 15 | restartPolicy: Never 16 | backoffLimit: 3 -------------------------------------------------------------------------------- /eks/deploy/rds/db-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: postgres-service 5 | spec: 6 | type: ExternalName 7 | externalName: -------------------------------------------------------------------------------- /eks/utils/alb-ingress-controller.yaml: -------------------------------------------------------------------------------- 1 | # Application Load Balancer (ALB) Ingress Controller Deployment Manifest. 2 | # This manifest details sensible defaults for deploying an ALB Ingress Controller. 3 | # GitHub: https://github.com/kubernetes-sigs/aws-alb-ingress-controller 4 | apiVersion: apps/v1 5 | kind: Deployment 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: alb-ingress-controller 9 | name: alb-ingress-controller 10 | # Namespace the ALB Ingress Controller should run in. Does not impact which 11 | # namespaces it's able to resolve ingress resource for. For limiting ingress 12 | # namespace scope, see --watch-namespace. 13 | namespace: kube-system 14 | spec: 15 | selector: 16 | matchLabels: 17 | app.kubernetes.io/name: alb-ingress-controller 18 | template: 19 | metadata: 20 | labels: 21 | app.kubernetes.io/name: alb-ingress-controller 22 | spec: 23 | containers: 24 | - name: alb-ingress-controller 25 | args: 26 | # Limit the namespace where this ALB Ingress Controller deployment will 27 | # resolve ingress resources. If left commented, all namespaces are used. 28 | # - --watch-namespace=your-k8s-namespace 29 | 30 | # Setting the ingress-class flag below ensures that only ingress resources with the 31 | # annotation kubernetes.io/ingress.class: "alb" are respected by the controller. You may 32 | # choose any class you'd like for this controller to respect. 33 | - --ingress-class=alb 34 | 35 | # REQUIRED 36 | # Name of your cluster. Used when naming resources created 37 | # by the ALB Ingress Controller, providing distinction between 38 | # clusters. 39 | # - --cluster-name=devCluster 40 | 41 | # AWS VPC ID this ingress controller will use to create AWS resources. 42 | # If unspecified, it will be discovered from ec2metadata. 43 | # - --aws-vpc-id=vpc-xxxxxx 44 | 45 | # AWS region this ingress controller will operate in. 46 | # If unspecified, it will be discovered from ec2metadata. 47 | # List of regions: http://docs.aws.amazon.com/general/latest/gr/rande.html#vpc_region 48 | # - --aws-region=us-west-1 49 | 50 | # Enables logging on all outbound requests sent to the AWS API. 51 | # If logging is desired, set to true. 52 | # - --aws-api-debug 53 | 54 | # Maximum number of times to retry the aws calls. 55 | # defaults to 10. 56 | # - --aws-max-retries=10 57 | env: 58 | # AWS key id for authenticating with the AWS API. 59 | # This is only here for examples. It's recommended you instead use 60 | # a project like kube2iam for granting access. 61 | # - name: AWS_ACCESS_KEY_ID 62 | # value: KEYVALUE 63 | 64 | # AWS key secret for authenticating with the AWS API. 65 | # This is only here for examples. It's recommended you instead use 66 | # a project like kube2iam for granting access. 67 | # - name: AWS_SECRET_ACCESS_KEY 68 | # value: SECRETVALUE 69 | # Repository location of the ALB Ingress Controller. 70 | image: docker.io/amazon/aws-alb-ingress-controller:v1.1.9 71 | serviceAccountName: alb-ingress-controller 72 | -------------------------------------------------------------------------------- /eks/utils/iam-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "acm:DescribeCertificate", 8 | "acm:ListCertificates", 9 | "acm:GetCertificate" 10 | ], 11 | "Resource": "*" 12 | }, 13 | { 14 | "Effect": "Allow", 15 | "Action": [ 16 | "ec2:AuthorizeSecurityGroupIngress", 17 | "ec2:CreateSecurityGroup", 18 | "ec2:CreateTags", 19 | "ec2:DeleteTags", 20 | "ec2:DeleteSecurityGroup", 21 | "ec2:DescribeAccountAttributes", 22 | "ec2:DescribeAddresses", 23 | "ec2:DescribeInstances", 24 | "ec2:DescribeInstanceStatus", 25 | "ec2:DescribeInternetGateways", 26 | "ec2:DescribeNetworkInterfaces", 27 | "ec2:DescribeSecurityGroups", 28 | "ec2:DescribeSubnets", 29 | "ec2:DescribeTags", 30 | "ec2:DescribeVpcs", 31 | "ec2:ModifyInstanceAttribute", 32 | "ec2:ModifyNetworkInterfaceAttribute", 33 | "ec2:RevokeSecurityGroupIngress" 34 | ], 35 | "Resource": "*" 36 | }, 37 | { 38 | "Effect": "Allow", 39 | "Action": [ 40 | "elasticloadbalancing:AddListenerCertificates", 41 | "elasticloadbalancing:AddTags", 42 | "elasticloadbalancing:CreateListener", 43 | "elasticloadbalancing:CreateLoadBalancer", 44 | "elasticloadbalancing:CreateRule", 45 | "elasticloadbalancing:CreateTargetGroup", 46 | "elasticloadbalancing:DeleteListener", 47 | "elasticloadbalancing:DeleteLoadBalancer", 48 | "elasticloadbalancing:DeleteRule", 49 | "elasticloadbalancing:DeleteTargetGroup", 50 | "elasticloadbalancing:DeregisterTargets", 51 | "elasticloadbalancing:DescribeListenerCertificates", 52 | "elasticloadbalancing:DescribeListeners", 53 | "elasticloadbalancing:DescribeLoadBalancers", 54 | "elasticloadbalancing:DescribeLoadBalancerAttributes", 55 | "elasticloadbalancing:DescribeRules", 56 | "elasticloadbalancing:DescribeSSLPolicies", 57 | "elasticloadbalancing:DescribeTags", 58 | "elasticloadbalancing:DescribeTargetGroups", 59 | "elasticloadbalancing:DescribeTargetGroupAttributes", 60 | "elasticloadbalancing:DescribeTargetHealth", 61 | "elasticloadbalancing:ModifyListener", 62 | "elasticloadbalancing:ModifyLoadBalancerAttributes", 63 | "elasticloadbalancing:ModifyRule", 64 | "elasticloadbalancing:ModifyTargetGroup", 65 | "elasticloadbalancing:ModifyTargetGroupAttributes", 66 | "elasticloadbalancing:RegisterTargets", 67 | "elasticloadbalancing:RemoveListenerCertificates", 68 | "elasticloadbalancing:RemoveTags", 69 | "elasticloadbalancing:SetIpAddressType", 70 | "elasticloadbalancing:SetSecurityGroups", 71 | "elasticloadbalancing:SetSubnets", 72 | "elasticloadbalancing:SetWebAcl" 73 | ], 74 | "Resource": "*" 75 | }, 76 | { 77 | "Effect": "Allow", 78 | "Action": [ 79 | "iam:CreateServiceLinkedRole", 80 | "iam:GetServerCertificate", 81 | "iam:ListServerCertificates" 82 | ], 83 | "Resource": "*" 84 | }, 85 | { 86 | "Effect": "Allow", 87 | "Action": [ 88 | "cognito-idp:DescribeUserPoolClient" 89 | ], 90 | "Resource": "*" 91 | }, 92 | { 93 | "Effect": "Allow", 94 | "Action": [ 95 | "waf-regional:GetWebACLForResource", 96 | "waf-regional:GetWebACL", 97 | "waf-regional:AssociateWebACL", 98 | "waf-regional:DisassociateWebACL" 99 | ], 100 | "Resource": "*" 101 | }, 102 | { 103 | "Effect": "Allow", 104 | "Action": [ 105 | "tag:GetResources", 106 | "tag:TagResources" 107 | ], 108 | "Resource": "*" 109 | }, 110 | { 111 | "Effect": "Allow", 112 | "Action": [ 113 | "waf:GetWebACL" 114 | ], 115 | "Resource": "*" 116 | }, 117 | { 118 | "Effect": "Allow", 119 | "Action": [ 120 | "wafv2:GetWebACL", 121 | "wafv2:GetWebACLForResource", 122 | "wafv2:AssociateWebACL", 123 | "wafv2:DisassociateWebACL" 124 | ], 125 | "Resource": "*" 126 | }, 127 | { 128 | "Effect": "Allow", 129 | "Action": [ 130 | "shield:DescribeProtection", 131 | "shield:GetSubscriptionState", 132 | "shield:DeleteProtection", 133 | "shield:CreateProtection", 134 | "shield:DescribeSubscription", 135 | "shield:ListProtections" 136 | ], 137 | "Resource": "*" 138 | } 139 | ] 140 | } -------------------------------------------------------------------------------- /eks/utils/rbac-role.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: alb-ingress-controller 7 | name: alb-ingress-controller 8 | rules: 9 | - apiGroups: 10 | - "" 11 | - extensions 12 | resources: 13 | - configmaps 14 | - endpoints 15 | - events 16 | - ingresses 17 | - ingresses/status 18 | - services 19 | - pods/status 20 | verbs: 21 | - create 22 | - get 23 | - list 24 | - update 25 | - watch 26 | - patch 27 | - apiGroups: 28 | - "" 29 | - extensions 30 | resources: 31 | - nodes 32 | - pods 33 | - secrets 34 | - services 35 | - namespaces 36 | verbs: 37 | - get 38 | - list 39 | - watch 40 | --- 41 | apiVersion: rbac.authorization.k8s.io/v1 42 | kind: ClusterRoleBinding 43 | metadata: 44 | labels: 45 | app.kubernetes.io/name: alb-ingress-controller 46 | name: alb-ingress-controller 47 | roleRef: 48 | apiGroup: rbac.authorization.k8s.io 49 | kind: ClusterRole 50 | name: alb-ingress-controller 51 | subjects: 52 | - kind: ServiceAccount 53 | name: alb-ingress-controller 54 | namespace: kube-system 55 | --- 56 | apiVersion: v1 57 | kind: ServiceAccount 58 | metadata: 59 | labels: 60 | app.kubernetes.io/name: alb-ingress-controller 61 | name: alb-ingress-controller 62 | namespace: kube-system 63 | ... -------------------------------------------------------------------------------- /k8s/celery/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: celery-deployment 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: celery-app 13 | template: 14 | metadata: 15 | labels: 16 | app: celery-app 17 | spec: 18 | initContainers: 19 | - name: init-redis-service 20 | image: busybox:1.28 21 | command: [ 'sh', '-c', "until nslookup redis-service.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for redis-service; sleep 2; done" ] 22 | 23 | containers: 24 | - image: 25 | command: ['celery', '-A', 'main.celery', 'worker', '-l', 'info'] 26 | envFrom: 27 | - secretRef: 28 | name: celery-secret 29 | name: celery-container 30 | 31 | 32 | -------------------------------------------------------------------------------- /k8s/celery/secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: celery-secret 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | data: 9 | REDIS_HOST: cmVkaXMtc2VydmljZQo= 10 | REDIS_PORT: NjM3OQo= 11 | REDIS_DB: MAo= 12 | -------------------------------------------------------------------------------- /k8s/code/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: ecommerce-deployment 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | spec: 9 | replicas: 8 10 | selector: 11 | matchLabels: 12 | app: ecommerce-app 13 | template: 14 | metadata: 15 | labels: 16 | app: ecommerce-app 17 | spec: 18 | initContainers: 19 | - name: init-postgres-service 20 | image: postgres:10.17 21 | command: ['sh', '-c', 22 | 'until pg_isready -h postgres-service.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local -p 5432; 23 | do echo waiting for database; sleep 2; done;'] 24 | 25 | containers: 26 | - image: 27 | imagePullPolicy: Always 28 | name: sample-container 29 | envFrom: 30 | - secretRef: 31 | name: ecommerce-secret 32 | ports: 33 | - containerPort: 5000 34 | name: fastapi 35 | readinessProbe: 36 | httpGet: 37 | port: 5000 38 | path: /docs 39 | initialDelaySeconds: 15 40 | livenessProbe: 41 | httpGet: 42 | port: 5000 43 | path: /docs 44 | initialDelaySeconds: 15 45 | periodSeconds: 15 46 | resources: 47 | requests: 48 | memory: "512Mi" 49 | cpu: "0.5" 50 | limits: 51 | memory: "1Gi" 52 | cpu: "1" 53 | -------------------------------------------------------------------------------- /k8s/code/secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: ecommerce-secret 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | data: 9 | DATABASE_USERNAME: bXVrdWxtYW50b3No 10 | DATABASE_PASSWORD: bXVrdWwxMjM= 11 | DATABASE_HOST: cG9zdGdyZXMtc2VydmljZQ== 12 | DATABASE_NAME: c2FtcGxlZGI= -------------------------------------------------------------------------------- /k8s/code/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: ecommerce-service 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | spec: 9 | selector: 10 | app: ecommerce-app 11 | ports: 12 | - port: 5000 13 | targetPort: 5000 14 | 15 | -------------------------------------------------------------------------------- /k8s/job/migration.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: fastapi-migrations 5 | namespace: fastapi-project 6 | spec: 7 | ttlSecondsAfterFinished: 100 8 | template: 9 | spec: 10 | containers: 11 | - name: migration-container 12 | image: 13 | command: ['alembic', 'upgrade', 'head'] 14 | envFrom: 15 | - secretRef: 16 | name: migration-secret 17 | initContainers: 18 | - name: init-postgres-service 19 | image: postgres:10.17 20 | command: [ 'sh', '-c', 21 | 'until pg_isready -h postgres-service.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local -p 5432; 22 | do echo waiting for database; sleep 2; done;' ] 23 | restartPolicy: OnFailure 24 | backoffLimit: 15 -------------------------------------------------------------------------------- /k8s/job/secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: migration-secret 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | data: 9 | DATABASE_USERNAME: bXVrdWxtYW50b3No 10 | DATABASE_PASSWORD: bXVrdWwxMjM= 11 | DATABASE_HOST: cG9zdGdyZXMtc2VydmljZQ== 12 | DATABASE_NAME: c2FtcGxlZGI= -------------------------------------------------------------------------------- /k8s/namespace/ns.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: fastapi-project 5 | labels: 6 | name: fastapi-project 7 | -------------------------------------------------------------------------------- /k8s/nginx/nginx-config.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: nginx-config 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | data: 9 | default.conf: | 10 | upstream ecommerce_project { 11 | server ecommerce-service:5000; 12 | } 13 | server { 14 | 15 | listen 80; 16 | 17 | location / { 18 | proxy_pass http://ecommerce_project; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | proxy_set_header Host $host; 21 | proxy_redirect off; 22 | } 23 | } -------------------------------------------------------------------------------- /k8s/nginx/nginx-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | spec: 9 | replicas: 8 10 | selector: 11 | matchLabels: 12 | app: ecommerce-nginx 13 | template: 14 | metadata: 15 | labels: 16 | app: ecommerce-nginx 17 | spec: 18 | containers: 19 | - image: nginx:1.21 20 | name: nginx-container 21 | ports: 22 | - containerPort: 80 23 | readinessProbe: 24 | httpGet: 25 | port: 80 26 | path: /docs 27 | initialDelaySeconds: 15 28 | livenessProbe: 29 | httpGet: 30 | port: 80 31 | path: /docs 32 | initialDelaySeconds: 15 33 | periodSeconds: 15 34 | volumeMounts: 35 | - name: nginx-config 36 | mountPath: /etc/nginx/conf.d/default.conf 37 | subPath: default.conf 38 | volumes: 39 | - name: nginx-config 40 | configMap: 41 | name: nginx-config 42 | -------------------------------------------------------------------------------- /k8s/nginx/nginx-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: nginx-service 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | spec: 9 | type: NodePort 10 | selector: 11 | app: ecommerce-nginx 12 | ports: 13 | - port: 80 14 | targetPort: 80 15 | nodePort: 30009 16 | 17 | -------------------------------------------------------------------------------- /k8s/password-alter.txt: -------------------------------------------------------------------------------- 1 | # sudo -u postgresql psql 2 | # postgresql=#ALTER USER yourusername WITH PASSWORD 3 | # 'set_new_password_without_special_character'; -------------------------------------------------------------------------------- /k8s/postgres/postgres-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: postgres-deployment 5 | namespace: fastapi-project 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: postgres-container 11 | template: 12 | metadata: 13 | labels: 14 | app: postgres-container 15 | tier: backend 16 | spec: 17 | containers: 18 | - name: postgres-container 19 | image: postgres:10.17 20 | envFrom: 21 | - secretRef: 22 | name: postgres-secret 23 | ports: 24 | - containerPort: 5432 25 | resources: 26 | requests: 27 | memory: "512Mi" 28 | cpu: "0.5" 29 | limits: 30 | memory: "1Gi" 31 | cpu: "1" 32 | volumeMounts: 33 | - name: postgres-volume-mount 34 | mountPath: /var/lib/postgresql/data 35 | volumes: 36 | - name: postgres-volume-mount 37 | persistentVolumeClaim: 38 | claimName: postgres-pvc -------------------------------------------------------------------------------- /k8s/postgres/postgres-pv.yml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolume 2 | apiVersion: v1 3 | metadata: 4 | name: postgres-pv 5 | namespace: fastapi-project 6 | labels: 7 | type: local 8 | app: ecommerce 9 | spec: 10 | persistentVolumeReclaimPolicy: Delete 11 | storageClassName: local-storage 12 | capacity: 13 | storage: 2Gi 14 | volumeMode: Filesystem 15 | accessModes: 16 | - ReadWriteMany 17 | local: 18 | path: /run/desktop/mnt/host/e/postgres-data # <-- if running with docker desktop in windows (pointing to "e" drive) 19 | nodeAffinity: 20 | required: 21 | nodeSelectorTerms: 22 | - matchExpressions: 23 | - key: kubernetes.io/hostname 24 | operator: In 25 | values: 26 | - docker-desktop # <-- name of the node 27 | -------------------------------------------------------------------------------- /k8s/postgres/postgres-pvc.yml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: postgres-pvc 5 | namespace: fastapi-project 6 | labels: 7 | type: local 8 | app: ecommerce 9 | spec: 10 | storageClassName: local-storage 11 | accessModes: 12 | - ReadWriteMany 13 | resources: 14 | requests: 15 | storage: 2Gi 16 | volumeName: postgres-pv -------------------------------------------------------------------------------- /k8s/postgres/postgres-secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: postgres-secret 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | data: 9 | POSTGRES_DB: c2FtcGxlZGI= 10 | POSTGRES_USER: bXVrdWxtYW50b3No 11 | POSTGRES_PASSWORD: bXVrdWwxMjM= 12 | -------------------------------------------------------------------------------- /k8s/postgres/postgres-service.yml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: postgres-service 5 | namespace: fastapi-project 6 | spec: 7 | selector: 8 | app: postgres-container 9 | ports: 10 | - protocol: TCP 11 | port: 5432 12 | targetPort: 5432 -------------------------------------------------------------------------------- /k8s/redis/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: redis-deployment 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: redis-app 13 | template: 14 | metadata: 15 | labels: 16 | app: redis-app 17 | spec: 18 | containers: 19 | - image: redis:6.2.5-alpine 20 | imagePullPolicy: IfNotPresent 21 | name: redis-container 22 | ports: 23 | - containerPort: 6379 24 | readinessProbe: 25 | tcpSocket: 26 | port: 6379 27 | livenessProbe: 28 | tcpSocket: 29 | port: 6379 30 | periodSeconds: 15 31 | resources: 32 | limits: 33 | memory: 256Mi 34 | cpu: 125m 35 | requests: 36 | cpu: 70m 37 | memory: 200Mi 38 | 39 | 40 | -------------------------------------------------------------------------------- /k8s/redis/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: redis-service 5 | namespace: fastapi-project 6 | labels: 7 | app: ecommerce 8 | spec: 9 | selector: 10 | app: redis-app 11 | ports: 12 | - port: 6379 13 | targetPort: 6379 14 | 15 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | from fastapi import FastAPI 3 | 4 | from ecommerce import config 5 | from ecommerce.auth import router as auth_router 6 | from ecommerce.cart import router as cart_router 7 | from ecommerce.orders import router as order_router 8 | from ecommerce.products import router as product_router 9 | from ecommerce.user import router as user_router 10 | 11 | 12 | description = """ 13 | Ecommerce API 14 | 15 | ## Users 16 | 17 | You will be able to: 18 | 19 | * **Create users** 20 | * **Read users** 21 | """ 22 | 23 | app = FastAPI( 24 | title="EcommerceApp", 25 | description=description, 26 | version="0.0.1", 27 | terms_of_service="http://example.com/terms/", 28 | contact={ 29 | "name": "Mukul Mantosh", 30 | "url": "http://x-force.example.com/contact/", 31 | }, 32 | license_info={ 33 | "name": "Apache 2.0", 34 | "url": "https://www.apache.org/licenses/LICENSE-2.0.html", 35 | }, 36 | ) 37 | app.include_router(auth_router.router) 38 | app.include_router(user_router.router) 39 | app.include_router(product_router.router) 40 | app.include_router(cart_router.router) 41 | app.include_router(order_router.router) 42 | 43 | celery = Celery( 44 | __name__, 45 | broker=f"redis://{config.REDIS_HOST}:{config.REDIS_PORT}/{config.REDIS_DB}", 46 | backend=f"redis://{config.REDIS_HOST}:{config.REDIS_PORT}/{config.REDIS_DB}" 47 | ) 48 | celery.conf.imports = [ 49 | 'ecommerce.orders.tasks', 50 | ] 51 | -------------------------------------------------------------------------------- /misc/images/celery-task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/misc/images/celery-task.png -------------------------------------------------------------------------------- /misc/images/env_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/misc/images/env_file.png -------------------------------------------------------------------------------- /misc/images/requirements.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/misc/images/requirements.gif -------------------------------------------------------------------------------- /misc/images/stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/misc/images/stack.png -------------------------------------------------------------------------------- /misc/images/testing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/misc/images/testing.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.6.5 2 | amqp==5.0.6 3 | anyio==3.3.0 4 | argon2-cffi==20.1.0 5 | asgi-lifespan==1.0.1 6 | asgiref==3.4.1 7 | attrs==21.2.0 8 | billiard==3.6.4.0 9 | boto3==1.18.26 10 | botocore==1.21.26 11 | celery==5.1.2 12 | certifi==2021.5.30 13 | cffi==1.14.6 14 | charset-normalizer==2.0.4 15 | click==7.1.2 16 | click-didyoumean==0.0.3 17 | click-plugins==1.1.1 18 | click-repl==0.2.0 19 | colorama==0.4.4 20 | coverage==5.5 21 | cryptography==3.4.7 22 | dnspython==2.1.0 23 | ecdsa==0.17.0 24 | email-validator==1.1.3 25 | Faker==8.12.0 26 | fastapi==0.68.0 27 | gunicorn==20.1.0 28 | h11==0.12.0 29 | httpcore==0.13.6 30 | httpx==0.19.0 31 | idna==3.2 32 | iniconfig==1.1.1 33 | jmespath==0.10.0 34 | kombu==5.1.0 35 | Mako==1.1.4 36 | MarkupSafe==2.0.1 37 | packaging==21.0 38 | passlib==1.7.4 39 | pluggy==0.13.1 40 | prompt-toolkit==3.0.20 41 | psycopg2==2.9.1 42 | py==1.10.0 43 | pyasn1==0.4.8 44 | pycparser==2.20 45 | pydantic==1.8.2 46 | pyparsing==2.4.7 47 | pytest==6.2.4 48 | pytest-asyncio==0.15.1 49 | pytest-mock==3.6.1 50 | python-dateutil==2.8.2 51 | python-editor==1.0.4 52 | python-jose==3.3.0 53 | python-multipart==0.0.5 54 | pytz==2021.1 55 | redis==3.5.3 56 | rfc3986==1.5.0 57 | rsa==4.7.2 58 | s3transfer==0.5.0 59 | six==1.16.0 60 | sniffio==1.2.0 61 | SQLAlchemy==1.3.24 62 | starlette==0.14.2 63 | text-unidecode==1.3 64 | toml==0.10.2 65 | typing-extensions==3.10.0.0 66 | urllib3==1.26.6 67 | uvicorn==0.14.0 68 | vine==5.0.0 69 | wcwidth==0.2.5 70 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/tests/__init__.py -------------------------------------------------------------------------------- /tests/cart/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/tests/cart/__init__.py -------------------------------------------------------------------------------- /tests/cart/test_cart.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from httpx import AsyncClient 3 | 4 | from ecommerce.auth.jwt import create_access_token 5 | from conf_test_db import app 6 | from tests.shared.info import category_info, product_info 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_add_to_cart(): 11 | async with AsyncClient(app=app, base_url="http://test") as ac: 12 | category_obj = await category_info() 13 | product_obj = await product_info(category_obj) 14 | user_access_token = create_access_token({"sub": "john@gmail.com"}) 15 | 16 | response = await ac.get(f"/cart/add", 17 | params={'product_id': product_obj.id}, 18 | headers={'Authorization': f'Bearer {user_access_token}'}) 19 | 20 | assert response.status_code == 201 21 | assert response.json() == {"status": "Item Added to Cart"} 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_cart_listing(): 26 | async with AsyncClient(app=app, base_url="http://test") as ac: 27 | user_access_token = create_access_token({"sub": "john@gmail.com"}) 28 | 29 | response = await ac.get(f"/cart/", headers={'Authorization': f'Bearer {user_access_token}'}) 30 | 31 | assert response.status_code == 200 32 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ecommerce.user.models import User 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def create_dummy_user(tmpdir): 8 | """Fixture to execute asserts before and after a test is run""" 9 | # Setup: fill with any logic you want 10 | from conf_test_db import override_get_db 11 | database = next(override_get_db()) 12 | new_user = User(name='John', email='john@gmail.com', password='john123') 13 | database.add(new_user) 14 | database.commit() 15 | 16 | yield # this is where the testing happens 17 | 18 | # Teardown : fill with any logic you want 19 | database.query(User).filter(User.email == 'john@gmail.com').delete() 20 | database.commit() 21 | -------------------------------------------------------------------------------- /tests/home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/tests/home/__init__.py -------------------------------------------------------------------------------- /tests/home/test_home.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from httpx import AsyncClient 3 | 4 | from conf_test_db import app 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_root(): 9 | async with AsyncClient(app=app, base_url="http://test") as ac: 10 | response = await ac.get("/") 11 | assert response.status_code == 404 12 | assert response.json() == {"detail": "Not Found"} -------------------------------------------------------------------------------- /tests/login/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/tests/login/__init__.py -------------------------------------------------------------------------------- /tests/login/test_login.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from httpx import AsyncClient 4 | 5 | from conf_test_db import app 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_login(): 10 | async with AsyncClient(app=app, base_url="http://test") as ac: 11 | response = await ac.post("/login", data={'username': 'john@gmail.com', 'password': 'john123'}) 12 | assert response.status_code == 200 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/orders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/tests/orders/__init__.py -------------------------------------------------------------------------------- /tests/orders/test_orders.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from httpx import AsyncClient 3 | 4 | from ecommerce.auth.jwt import create_access_token 5 | from conf_test_db import app 6 | from tests.shared.info import category_info, product_info 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_order_processing(mocker): 11 | mocker.patch('ecommerce.orders.tasks.send_email', return_value=True) 12 | 13 | async with AsyncClient(app=app, base_url="http://test") as ac: 14 | user_access_token = create_access_token({"sub": "john@gmail.com"}) 15 | category_obj = await category_info() 16 | product_obj = await product_info(category_obj) 17 | 18 | cart_response = await ac.get(f"/cart/add", 19 | params={'product_id': product_obj.id}, 20 | headers={'Authorization': f'Bearer {user_access_token}'}) 21 | 22 | order_response = await ac.post("/orders/", headers={'Authorization': f'Bearer {user_access_token}'}) 23 | 24 | assert cart_response.status_code == 201 25 | assert order_response.status_code == 201 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_order_listing(): 30 | async with AsyncClient(app=app, base_url="http://test") as ac: 31 | user_access_token = create_access_token({"sub": "john@gmail.com"}) 32 | response = await ac.get("/orders/", headers={'Authorization': f'Bearer {user_access_token}'}) 33 | 34 | assert response.status_code == 200 35 | -------------------------------------------------------------------------------- /tests/products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/tests/products/__init__.py -------------------------------------------------------------------------------- /tests/products/test_categories.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from httpx import AsyncClient 3 | 4 | from ecommerce.products.models import Category 5 | from conf_test_db import app, override_get_db 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_new_category(): 10 | async with AsyncClient(app=app, base_url="http://test") as ac: 11 | response = await ac.post("/products/category", json={'name': 'Apparels'}) 12 | assert response.status_code == 201 13 | assert response.json()['name'] == "Apparels" 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_list_get_category(): 18 | async with AsyncClient(app=app, base_url="http://test") as ac: 19 | database = next(override_get_db()) 20 | new_category = Category(name="Food") 21 | database.add(new_category) 22 | database.commit() 23 | database.refresh(new_category) 24 | first_response = await ac.get("/products/category") 25 | second_response = await ac.get(f"/products/category/{new_category.id}") 26 | assert first_response.status_code == 200 27 | assert second_response.status_code == 200 28 | assert second_response.json() == {"id": new_category.id, "name": new_category.name} 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_delete_category(): 33 | async with AsyncClient(app=app, base_url="http://test") as ac: 34 | database = next(override_get_db()) 35 | new_category = Category(name="Electronics") 36 | database.add(new_category) 37 | database.commit() 38 | database.refresh(new_category) 39 | response = await ac.delete(f"/products/category/{new_category.id}") 40 | assert response.status_code == 204 41 | -------------------------------------------------------------------------------- /tests/products/test_products.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from httpx import AsyncClient 3 | 4 | from conf_test_db import app 5 | from tests.shared.info import category_info, product_info 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_new_product(): 10 | async with AsyncClient(app=app, base_url="http://test") as ac: 11 | category_obj = await category_info() 12 | payload = { 13 | "name": "Quaker Oats", 14 | "quantity": 4, 15 | "description": "Quaker: Good Quality Oats", 16 | "price": 10, 17 | "category_id": category_obj.id 18 | } 19 | 20 | response = await ac.post("/products/", json=payload) 21 | assert response.status_code == 201 22 | assert response.json()['name'] == "Quaker Oats" 23 | assert response.json()['quantity'] == 4 24 | assert response.json()['description'] == "Quaker: Good Quality Oats" 25 | assert response.json()['price'] == 10 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_list_products(): 30 | async with AsyncClient(app=app, base_url="http://test") as ac: 31 | category_obj = await category_info() 32 | await product_info(category_obj) 33 | 34 | response = await ac.get("/products/") 35 | assert response.status_code == 200 36 | assert 'name' in response.json()[0] 37 | assert 'quantity' in response.json()[0] 38 | assert 'description' in response.json()[0] 39 | assert 'price' in response.json()[0] 40 | -------------------------------------------------------------------------------- /tests/registration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/tests/registration/__init__.py -------------------------------------------------------------------------------- /tests/registration/test_user_registration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from faker import Faker 4 | from httpx import AsyncClient 5 | 6 | from conf_test_db import app 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_registration(): 11 | fake = Faker() 12 | data = { 13 | "name": fake.name(), 14 | "email": fake.email(), 15 | "password": fake.password() 16 | } 17 | async with AsyncClient(app=app, base_url="http://test") as ac: 18 | response = await ac.post("/user/", json=data) 19 | assert response.status_code == 201 20 | -------------------------------------------------------------------------------- /tests/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/tests/shared/__init__.py -------------------------------------------------------------------------------- /tests/shared/info.py: -------------------------------------------------------------------------------- 1 | from faker import Faker 2 | 3 | from ecommerce.products.models import Category, Product 4 | from conf_test_db import override_get_db 5 | 6 | 7 | async def category_info() -> Category: 8 | fake = Faker() 9 | database = next(override_get_db()) 10 | category_count = database.query(Category).filter().count() 11 | if category_count <= 0: 12 | category_obj = Category(name=fake.name()) 13 | database.add(category_obj) 14 | database.commit() 15 | database.refresh(category_obj) 16 | 17 | else: 18 | category_obj = database.query(Category).order_by(Category.id.desc()).first() 19 | return category_obj 20 | 21 | 22 | async def product_info(category_obj: Category) -> Product: 23 | database = next(override_get_db()) 24 | 25 | payload = { 26 | "name": "Quaker Oats", 27 | "quantity": 4, 28 | "description": "Quaker: Good Quality Oats", 29 | "price": 10, 30 | "category_id": category_obj.id 31 | } 32 | new_product = Product(**payload) 33 | database.add(new_product) 34 | database.commit() 35 | return new_product 36 | -------------------------------------------------------------------------------- /tests/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mukulmantosh/FastAPI_EKS_Kubernetes/9d1927bdb516fb145e48689a819300685eb4711a/tests/user/__init__.py -------------------------------------------------------------------------------- /tests/user/test_user.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from httpx import AsyncClient 3 | 4 | from ecommerce.auth.jwt import create_access_token 5 | from conf_test_db import app 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_all_users(): 10 | async with AsyncClient(app=app, base_url="http://test") as ac: 11 | user_access_token = create_access_token({"sub": "john@gmail.com"}) 12 | response = await ac.get("/user/", headers={'Authorization': f'Bearer {user_access_token}'}) 13 | assert response.status_code == 200 14 | --------------------------------------------------------------------------------