feat(api): added rate limiting

- Need update on testing env
This commit is contained in:
minhtrannhat 2024-01-24 21:09:15 -05:00
parent dd0bcbc984
commit ec6e39bd7c
Signed by: minhtrannhat
GPG Key ID: E13CFA85C53F8062
6 changed files with 201 additions and 12 deletions

View File

@ -1 +1,5 @@
# Backend Technical Write Up
## Structure
- Use FastAPI's `router` to organize different API routes

View File

@ -34,4 +34,4 @@ COPY --from=builder /backend/__pypackages__/3.11/bin/* /bin/
EXPOSE 8080
# set command/entrypoint, adapt to fit your needs
CMD ["python3","-m", "uvicorn", "neo_neo_todo.main:app", "--reload"]
CMD ["python3","-m", "uvicorn", "neo_neo_todo.main:app"]

181
backend/pdm.lock generated
View File

@ -5,7 +5,7 @@
groups = ["default", "dev"]
strategy = ["cross_platform"]
lock_version = "4.4.1"
content_hash = "sha256:c581daeea4062304392ead8d824c1a63198d7256615f32e18c6d0716eafbb0cb"
content_hash = "sha256:c967cac24f6f70dbbca2f704a73e986d1e00e940145b4dc6471c8bfd85440e79"
[[package]]
name = "annotated-types"
@ -31,6 +31,51 @@ files = [
{file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
]
[[package]]
name = "async-timeout"
version = "4.0.3"
requires_python = ">=3.7"
summary = "Timeout context manager for asyncio programs"
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
[[package]]
name = "bcrypt"
version = "4.1.2"
requires_python = ">=3.7"
summary = "Modern password hashing for your software and your servers"
files = [
{file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"},
{file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"},
{file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"},
{file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"},
{file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"},
{file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"},
{file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"},
{file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"},
{file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"},
{file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"},
{file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"},
{file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"},
{file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"},
{file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"},
{file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"},
{file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"},
{file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"},
{file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"},
{file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"},
{file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"},
{file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"},
{file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"},
{file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"},
{file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"},
{file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"},
{file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"},
{file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"},
]
[[package]]
name = "black"
version = "23.12.1"
@ -89,6 +134,30 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "dnspython"
version = "2.5.0"
requires_python = ">=3.8"
summary = "DNS toolkit"
files = [
{file = "dnspython-2.5.0-py3-none-any.whl", hash = "sha256:6facdf76b73c742ccf2d07add296f178e629da60be23ce4b0a9c927b1e02c3a6"},
{file = "dnspython-2.5.0.tar.gz", hash = "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15"},
]
[[package]]
name = "email-validator"
version = "2.1.0.post1"
requires_python = ">=3.8"
summary = "A robust email address syntax and deliverability validation library."
dependencies = [
"dnspython>=2.0.0",
"idna>=2.0.0",
]
files = [
{file = "email_validator-2.1.0.post1-py3-none-any.whl", hash = "sha256:c973053efbeddfef924dc0bd93f6e77a1ea7ee0fce935aea7103c7a3d6d2d637"},
{file = "email_validator-2.1.0.post1.tar.gz", hash = "sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44"},
]
[[package]]
name = "fastapi"
version = "0.109.0"
@ -104,6 +173,33 @@ files = [
{file = "fastapi-0.109.0.tar.gz", hash = "sha256:b978095b9ee01a5cf49b19f4bc1ac9b8ca83aa076e770ef8fd9af09a2b88d191"},
]
[[package]]
name = "fastapi-limiter"
version = "0.1.6"
requires_python = ">=3.9,<4.0"
summary = "A request rate limiter for fastapi"
dependencies = [
"fastapi",
"redis>=4.2.0rc1",
]
files = [
{file = "fastapi_limiter-0.1.6-py3-none-any.whl", hash = "sha256:2e53179a4208b8f2c8795e38bb001324d3dc37d2800ff49fd28ec5caabf7a240"},
{file = "fastapi_limiter-0.1.6.tar.gz", hash = "sha256:6f5fde8efebe12eb33861bdffb91009f699369a3c2862cdc7c1d9acf912ff443"},
]
[[package]]
name = "freezegun"
version = "1.4.0"
requires_python = ">=3.7"
summary = "Let your Python tests travel through time"
dependencies = [
"python-dateutil>=2.7",
]
files = [
{file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"},
{file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"},
]
[[package]]
name = "h11"
version = "0.14.0"
@ -181,6 +277,16 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "itsdangerous"
version = "2.1.2"
requires_python = ">=3.7"
summary = "Safely pass data to untrusted environments and back."
files = [
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
@ -314,6 +420,21 @@ files = [
{file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"},
]
[[package]]
name = "pydantic"
version = "2.5.3"
extras = ["email"]
requires_python = ">=3.7"
summary = "Data validation using Python type hints"
dependencies = [
"email-validator>=2.0.0",
"pydantic==2.5.3",
]
files = [
{file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"},
{file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"},
]
[[package]]
name = "pytest"
version = "7.4.4"
@ -343,6 +464,19 @@ files = [
{file = "pytest_asyncio-0.23.3-py3-none-any.whl", hash = "sha256:37a9d912e8338ee7b4a3e917381d1c95bfc8682048cb0fbc35baba316ec1faba"},
]
[[package]]
name = "python-dateutil"
version = "2.8.2"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
summary = "Extensions to the standard Python datetime module"
dependencies = [
"six>=1.5",
]
files = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
[[package]]
name = "python-dotenv"
version = "1.0.0"
@ -376,6 +510,19 @@ files = [
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]]
name = "redis"
version = "5.1.0b3"
requires_python = ">=3.8"
summary = "Python client for Redis database and key-value store"
dependencies = [
"async-timeout>=4.0.3",
]
files = [
{file = "redis-5.1.0b3-py3-none-any.whl", hash = "sha256:8e83465ce69eb7b86b96d4e793ad1a8888d368815e47f1f6081d2d65d655a89c"},
{file = "redis-5.1.0b3.tar.gz", hash = "sha256:e5386f40168f16e4b136aa03a74cb13bccfb042280fd443f81482fc10548aae6"},
]
[[package]]
name = "ruff"
version = "0.1.14"
@ -401,6 +548,16 @@ files = [
{file = "ruff-0.1.14.tar.gz", hash = "sha256:ad3f8088b2dfd884820289a06ab718cde7d38b94972212cc4ba90d5fbc9955f3"},
]
[[package]]
name = "six"
version = "1.16.0"
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
summary = "Python 2 and 3 compatibility utilities"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "sniffio"
version = "1.3.0"
@ -436,7 +593,7 @@ files = [
[[package]]
name = "uvicorn"
version = "0.26.0"
version = "0.27.0"
requires_python = ">=3.8"
summary = "The lightning-fast ASGI server."
dependencies = [
@ -444,13 +601,13 @@ dependencies = [
"h11>=0.8",
]
files = [
{file = "uvicorn-0.26.0-py3-none-any.whl", hash = "sha256:cdb58ef6b8188c6c174994b2b1ba2150a9a8ae7ea5fb2f1b856b94a815d6071d"},
{file = "uvicorn-0.26.0.tar.gz", hash = "sha256:48bfd350fce3c5c57af5fb4995fded8fb50da3b4feb543eb18ad7e0d54589602"},
{file = "uvicorn-0.27.0-py3-none-any.whl", hash = "sha256:890b00f6c537d58695d3bb1f28e23db9d9e7a17cbcc76d7457c499935f933e24"},
{file = "uvicorn-0.27.0.tar.gz", hash = "sha256:c855578045d45625fd027367f7653d249f7c49f9361ba15cf9624186b26b8eb6"},
]
[[package]]
name = "uvicorn"
version = "0.26.0"
version = "0.27.0"
extras = ["standard"]
requires_python = ">=3.8"
summary = "The lightning-fast ASGI server."
@ -459,14 +616,14 @@ dependencies = [
"httptools>=0.5.0",
"python-dotenv>=0.13",
"pyyaml>=5.1",
"uvicorn==0.26.0",
"uvicorn==0.27.0",
"uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"",
"watchfiles>=0.13",
"websockets>=10.4",
]
files = [
{file = "uvicorn-0.26.0-py3-none-any.whl", hash = "sha256:cdb58ef6b8188c6c174994b2b1ba2150a9a8ae7ea5fb2f1b856b94a815d6071d"},
{file = "uvicorn-0.26.0.tar.gz", hash = "sha256:48bfd350fce3c5c57af5fb4995fded8fb50da3b4feb543eb18ad7e0d54589602"},
{file = "uvicorn-0.27.0-py3-none-any.whl", hash = "sha256:890b00f6c537d58695d3bb1f28e23db9d9e7a17cbcc76d7457c499935f933e24"},
{file = "uvicorn-0.27.0.tar.gz", hash = "sha256:c855578045d45625fd027367f7653d249f7c49f9361ba15cf9624186b26b8eb6"},
]
[[package]]
@ -552,3 +709,11 @@ files = [
{file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"},
{file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"},
]
[[package]]
name = "zxcvbn"
version = "4.4.28"
summary = ""
files = [
{file = "zxcvbn-4.4.28.tar.gz", hash = "sha256:151bd816817e645e9064c354b13544f85137ea3320ca3be1fb6873ea75ef7dc1"},
]

View File

@ -9,7 +9,11 @@ dependencies = [
"fastapi>=0.103.1",
"uvicorn[standard]>=0.23.2",
"httpx>=0.26.0",
"pydantic>=2.5.3",
"pydantic[email]>=2.5.3",
"bcrypt>=4.1.2", # hashing passwords
"zxcvbn>=4.4.28", # rate password strength
"itsdangerous>=2.1.2", # signing user tokens
"fastapi-limiter>=0.1.6",
]
requires-python = ">=3.11"
readme = "README.md"
@ -25,6 +29,7 @@ dev = [
"black>=23.9.1",
"pytest>=7.4.2",
"pytest-asyncio>=0.21.1",
"freezegun>=1.4.0",
]
[tool.ruff]

View File

@ -1,6 +1,9 @@
from fastapi import APIRouter
router = APIRouter(prefix="/control", tags=["control"])
router = APIRouter(
prefix="/control",
tags=["control"],
)
@router.get("/ping")

View File

@ -1,8 +1,20 @@
from contextlib import asynccontextmanager
import redis.asyncio as redis
from fastapi import FastAPI
from fastapi_limiter import FastAPILimiter
from src.neo_neo_todo.control import control
app = FastAPI()
@asynccontextmanager
async def lifespan(_: FastAPI):
redis_connection = redis.from_url("redis://localhost:6379", encoding="utf8")
await FastAPILimiter.init(redis_connection)
yield
await FastAPILimiter.close()
app = FastAPI(lifespan=lifespan)
app.include_router(control.router)