Skip to content

Commit

Permalink
OCT-2219: Allocations/ endpoints migration to FastAPI (#569)
Browse files Browse the repository at this point in the history
## Description
Migration of the following endpoints to use FastAPI instead of Flask implementation.
```
   + GET /allocations/epoch/{epoch}
   + GET /allocations/users/{address}/allocation_nonce
   + POST /allocations/allocate
   + GET /allocations/donors/{epoch}
   + GET /allocations/project/{proposal_address}/epoch/{epoch}
   + GET /allocations/user/{user_address}/epoch/{epoch}
 ```
  • Loading branch information
adam-gf authored Dec 3, 2024
1 parent 33675d5 commit dbdc2be
Show file tree
Hide file tree
Showing 20 changed files with 3,401 additions and 1,887 deletions.
4,139 changes: 2,313 additions & 1,826 deletions backend/poetry.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ uvloop = "^0.20.0"
python-socketio = "^5.11.4"

[tool.poetry.group.dev.dependencies]
pytest = "^7.3.1"
pytest = "^8.3.3"
pytest-mock = "^3.10.0"
black = "^23.3.0"
pytest-cov = "^4.0.0"
Expand All @@ -55,6 +55,10 @@ isort = "^5.13.2"
mypy = "^1.11.2"
ruff = "^0.6.2"
aiosqlite = "^0.20.0"
pytest-asyncio = "^0.24.0"
pytest-anyio = "^0.0.0"
trio = "^0.27.0"
httpx = "^0.27.2"

[tool.poetry.group.prod]
optional = true
Expand Down
8 changes: 5 additions & 3 deletions backend/startup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from fastapi import Request
from fastapi.middleware.wsgi import WSGIMiddleware

from fastapi.routing import Match

from starlette.middleware.base import BaseHTTPMiddleware

Expand Down Expand Up @@ -63,8 +63,10 @@ async def dispatch(self, request: Request, call_next):
path = request.url.path

for route in fastapi_app.routes:
if path == route.path:
# If path exists, proceed with the request
match, _ = route.matches(
{"type": "http", "path": path, "method": request.method}
)
if match != Match.NONE:
return await call_next(request)

# If path does not exist, modify the request to forward to the Flask app
Expand Down
128 changes: 118 additions & 10 deletions backend/tests/api-e2e/test_api_allocations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi.testclient import TestClient
import pytest
from flask import current_app as app

from fastapi import status
from app.legacy.core.projects import get_projects_addresses
from tests.conftest import Client, UserAccount
from tests.helpers.constants import STARTING_EPOCH, LOW_UQ_SCORE
Expand All @@ -9,6 +10,7 @@
@pytest.mark.api
def test_allocations(
client: Client,
fastapi_client: TestClient,
deployer: UserAccount,
ua_alice: UserAccount,
ua_bob: UserAccount,
Expand All @@ -31,9 +33,32 @@ def test_allocations(
res = client.pending_snapshot()
assert res["epoch"] > 0

ua_alice.allocate(1000, alice_proposals)
# Alice makes an allocation
ua_alice_nonce, _ = ua_alice._client.get_allocation_nonce(ua_alice.address)
signature = ua_alice._client.sign_operation(
ua_alice._account, 1000, alice_proposals, ua_alice_nonce
)
rv = fastapi_client.post(
"/allocations/allocate",
json={
"payload": {
"allocations": [
{"proposalAddress": address, "amount": 1000}
for address in alice_proposals
],
"nonce": ua_alice_nonce,
},
"userAddress": ua_alice.address,
"signature": signature,
"isManuallyEdited": False,
},
)
assert rv.status_code == status.HTTP_201_CREATED

# ua_alice.allocate(1000, alice_proposals)
ua_bob.allocate(1000, alice_proposals[:1])

# Check allocations using Flask API
allocations, _ = client.get_epoch_allocations(STARTING_EPOCH)
unique_donors = set()
unique_proposals = set()
Expand All @@ -48,9 +73,33 @@ def test_allocations(
assert len(unique_donors) == 2
assert len(unique_proposals) == 3

# Check allocations using FastAPI
resp = fastapi_client.get(f"/allocations/epoch/{STARTING_EPOCH}")
assert resp.status_code == status.HTTP_200_OK

fastapi_allocations = resp.json()
fastapi_unique_donors = set()
fastapi_unique_proposals = set()
app.logger.debug(f"FastAPI allocations: \n {fastapi_allocations}")

assert len(fastapi_allocations["allocations"]) == 4
for allocation in fastapi_allocations["allocations"]:
fastapi_unique_donors.add(allocation["donor"])
fastapi_unique_proposals.add(allocation["project"])
assert int(allocation["amount"]) > 0
assert len(fastapi_unique_donors) == 2
assert len(fastapi_unique_proposals) == 3

# 1:1 API check - Flask and FastAPI allocations are the same
assert unique_donors == fastapi_unique_donors
assert unique_proposals == fastapi_unique_proposals


def _check_allocations_logic(
client: Client, ua_alice: UserAccount, target_pending_epoch: int
client: Client,
ua_alice: UserAccount,
target_pending_epoch: int,
fastapi_client: TestClient,
):
alice_proposals = get_projects_addresses(1)[:3]

Expand All @@ -73,12 +122,19 @@ def _check_allocations_logic(
# Making allocations
nonce, status_code = client.get_allocation_nonce(ua_alice.address)
# Nonce is always 0
assert status_code == 200, "Nonce status code is different than 200"
assert status_code == status.HTTP_200_OK, "Nonce status code is different than 200"

# 1:1 API check - Flask and FastAPI nonce are the same
resp = fastapi_client.get(f"/allocations/users/{ua_alice.address}/allocation_nonce")
assert resp.status_code == status.HTTP_200_OK
assert resp.json() == {
"allocationNonce": nonce
}, "Flask and FastAPI nonce are different"

allocation_amount = 1000
allocation_response_code = ua_alice.allocate(allocation_amount, alice_proposals)
assert (
allocation_response_code == 201
allocation_response_code == status.HTTP_201_CREATED
), "Allocation status code is different than 201"

epoch_allocations, status_code = client.get_epoch_allocations(target_pending_epoch)
Expand All @@ -90,15 +146,38 @@ def _check_allocations_logic(
assert allocation["project"], "Proposal address is empty"
app.logger.debug(f"Allocations in epoch 1: {epoch_allocations}")

assert status_code == 200, "Status code is different than 200"
assert status_code == status.HTTP_200_OK, "Status code is different than 200"

# 1:1 API check - Flask and FastAPI allocations are the same
resp = fastapi_client.get(f"/allocations/epoch/{target_pending_epoch}")
assert resp.status_code == status.HTTP_200_OK
fastapi_epoch_allocations = resp.json()
assert len(fastapi_epoch_allocations["allocations"]) == len(alice_proposals)
for allocation in fastapi_epoch_allocations["allocations"]:
assert allocation["donor"] == ua_alice.address, "Donor address is wrong"
assert int(allocation["amount"]) == allocation_amount
assert allocation["project"], "Proposal address is empty"

assert resp.status_code == status.HTTP_200_OK

# Check user donations
user_allocations, status_code = client.get_user_allocations(
target_pending_epoch, ua_alice.address
)
app.logger.debug(f"User allocations: {user_allocations}")
assert user_allocations["allocations"], "User allocations for given epoch are empty"
assert status_code == 200, "Status code is different than 200"
assert status_code == status.HTTP_200_OK, "Status code is different than 200"

# 1:1 API check - Flask and FastAPI user allocations are the same
resp = fastapi_client.get(
f"/allocations/user/{ua_alice.address}/epoch/{target_pending_epoch}"
)
assert resp.status_code == status.HTTP_200_OK
fastapi_user_allocations = resp.json()
assert len(fastapi_user_allocations["allocations"]) == len(alice_proposals)
for allocation in fastapi_user_allocations["allocations"]:
assert allocation["address"] in alice_proposals
assert allocation["amount"] == str(allocation_amount)

# Check donors
donors, status_code = client.get_donors(target_pending_epoch)
Expand All @@ -107,6 +186,13 @@ def _check_allocations_logic(
assert donor == ua_alice.address, "Donor address is wrong"
assert status_code == 200, "Status code is different than 200"

# 1:1 API check - Flask and FastAPI donors are the same
resp = fastapi_client.get(f"/allocations/donors/{target_pending_epoch}")
assert resp.status_code == status.HTTP_200_OK
fastapi_donors = resp.json()
assert len(fastapi_donors["donors"]) == 1
assert fastapi_donors["donors"][0] == ua_alice.address

proposal_address = alice_proposals[0]
# Check donors of particular proposal
proposal_donors, status_code = client.get_proposal_donors(
Expand All @@ -119,6 +205,16 @@ def _check_allocations_logic(
), "Proposal donor address is wrong"
assert status_code == 200, "Status code is different than 200"

# 1:1 API check - Flask and FastAPI proposal donors are the same
resp = fastapi_client.get(
f"/allocations/project/{proposal_address}/epoch/{target_pending_epoch}"
)
assert resp.status_code == status.HTTP_200_OK
fastapi_proposal_donors = resp.json()
assert len(fastapi_proposal_donors) == 1
assert fastapi_proposal_donors[0]["address"] == ua_alice.address
assert fastapi_proposal_donors[0]["amount"] == str(allocation_amount)

# Check leverage
leverage, status_code = client.check_leverage(
proposal_address, ua_alice.address, 1000
Expand All @@ -135,24 +231,36 @@ def _check_allocations_logic(
@pytest.mark.api
def test_allocations_basics(
client: Client,
fastapi_client: TestClient,
deployer: UserAccount,
ua_alice: UserAccount,
ua_bob: UserAccount,
setup_funds,
):
_check_allocations_logic(client, ua_alice, target_pending_epoch=1)
_check_allocations_logic(
client, ua_alice, target_pending_epoch=1, fastapi_client=fastapi_client
)


@pytest.mark.api
def test_qf_and_uq_allocations(client: Client, ua_alice: UserAccount):
def test_qf_and_uq_allocations(
client: Client,
fastapi_client: TestClient,
ua_alice: UserAccount,
):
"""
Test for QF and UQ allocations.
This test checks if we use the QF alongside with UQ functionality properly.
Introduced in E4.
"""
PENDING_EPOCH = STARTING_EPOCH + 3

_check_allocations_logic(client, ua_alice, target_pending_epoch=PENDING_EPOCH)
_check_allocations_logic(
client,
ua_alice,
target_pending_epoch=PENDING_EPOCH,
fastapi_client=fastapi_client,
)

# Check if UQ is saved in the database after the allocation properly
res, code = client.get_user_uq(ua_alice.address, 4)
Expand Down
21 changes: 19 additions & 2 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from http import HTTPStatus
from unittest.mock import MagicMock, Mock

from fastapi.testclient import TestClient
import gql
import pytest
from flask import current_app
Expand All @@ -19,6 +20,7 @@
from web3 import Web3

import logging
from v2.main import app as fastapi_app
from app import create_app
from app.engine.user.effective_deposit import DepositEvent, EventType, UserDeposit
from app.exceptions import ExternalApiException
Expand Down Expand Up @@ -416,6 +418,21 @@ def random_string() -> str:
return "".join(random.choices(characters, k=length_of_string))


@pytest.fixture
def fastapi_client(deployment) -> TestClient:
# take SQLALCHEMY_DATABASE_URI and use as DB_URI
os.environ["DB_URI"] = deployment.SQLALCHEMY_DATABASE_URI
os.environ["PROPOSALS_CONTRACT_ADDRESS"] = deployment.PROJECTS_CONTRACT_ADDRESS

for key in dir(deployment):
if key.isupper():
value = getattr(deployment, key)
if value is not None:
os.environ[key] = str(value)

return TestClient(fastapi_app)


@pytest.fixture
def flask_client(deployment) -> FlaskClient:
"""An application for the integration / API tests."""
Expand Down Expand Up @@ -656,12 +673,12 @@ def get_rewards_budget(self, address: str, epoch: int):

def get_user_rewards_in_upcoming_epoch(self, address: str):
rv = self._flask_client.get(f"/rewards/budget/{address}/upcoming")
current_app.logger.debug("get_user_rewards_in_upcoming_epoch :", rv.text)
current_app.logger.debug(f"get_user_rewards_in_upcoming_epoch :{rv.text}")
return json.loads(rv.text)

def get_user_rewards_in_epoch(self, address: str, epoch: int):
rv = self._flask_client.get(f"/rewards/budget/{address}/epoch/{epoch}")
current_app.logger.debug("get_rewards_budget :", rv.text)
current_app.logger.debug(f"get_rewards_budget :{rv.text}")
return json.loads(rv.text)

def get_total_users_rewards_in_epoch(self, epoch):
Expand Down
Loading

0 comments on commit dbdc2be

Please sign in to comment.