Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCT-1825: Initial passthrough FastAPI server #379

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
db,
migrate,
cors,
socketio,
cache,
init_web3,
api,
Expand Down Expand Up @@ -47,7 +46,7 @@ def register_extensions(app):
cors.init_app(app)
db.init_app(app)
migrate.init_app(app, db)
socketio.init_app(app)
# socketio.init_app(app)
cache.init_app(app)
init_scheduler(app)
init_logger(app)
Expand Down
5 changes: 5 additions & 0 deletions backend/app/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ def config(app_level):
"apscheduler.executors.default": {
"level": "WARNING",
},
"uvicorn": { # Adding for the uvicorn logger (FastAPI)
"level": app_level,
"handlers": ["stdout", "stderr"],
"propagate": 0,
},
},
}

Expand Down
2 changes: 1 addition & 1 deletion backend/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class DevConfig(Config):

ENV = "dev"
DEBUG = True
LOG_LVL = os.getenv("OCTANT_LOG_LEVEL", "DEBUG")
LOG_LVL = os.getenv("OCTANT_LOG_LEVEL", "INFO")
DB_NAME = "dev.db"
CHAIN_ID = int(os.getenv("CHAIN_ID", 1337))
# Put the db file in project root
Expand Down
199 changes: 190 additions & 9 deletions backend/poetry.lock

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ pandas = "^2.2.0"
gmpy2 = "^2.1.5"
sentry-sdk = {extras = ["flask"], version = "^2.5.1"}
redis = "^5.0.7"
fastapi = "^0.112.0"
mypy = "^1.11.2"
isort = "^5.13.2"
pydantic-settings = "^2.4.0"
uvicorn = "^0.30.6"

[tool.poetry.group.dev.dependencies]
pytest = "^7.3.1"
Expand All @@ -43,6 +48,10 @@ pyright = "^1.1.366"
pylookup = "^0.2.2"
importmagic = "^0.1.7"
epc = "^0.0.5"
isort = "^5.13.2"
mypy = "^1.11.2"
ruff = "^0.6.2"
aiosqlite = "^0.20.0"

[tool.poetry.group.prod]
optional = true
Expand Down
85 changes: 85 additions & 0 deletions backend/socket_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import asyncio
import socketio

# Create a Socket.IO client
sio = socketio.AsyncClient(logger=True, engineio_logger=True)


# Define event handlers
@sio.event
async def connect():
print(">>>Connected to the server")


@sio.event
async def connect_error(data):
print(">>>The connection failed with error:", data)


@sio.event
async def disconnect():
print(">>>I'm disconnected!")


# A handler for any event with a wildcard (not all implementations of Socket.IO support this feature directly)
@sio.on("*")
async def catch_all(event, data):
print(f">>>Received an event of type '{event}' with data:", data)


@sio.event
async def epoch(data):
print(f">>>Epoch received: {data}")


@sio.event
async def project_rewards(data):
print(f"Message received: {data}")


@sio.event
async def threshold(data):
print(f"Custom event received: {data}")


# Connect to the server
async def main():
print("Connecting to the server...")
await sio.connect("http://localhost:8000/", wait_timeout=10)
print("Connected. Waiting for events...")
# This line will keep the client running and listening for events

# Emit events

# Emit a custom event
data = {
"userAddress": "0xb429d71F676f6e804010D8B699EefbF1ed050420",
"payload": {
"allocations": [
{
"proposalAddress": "0x1c01595f9534E33d411035AE99a4317faeC4f6Fe",
"amount": 100,
},
{
"proposalAddress": "0x6e8873085530406995170Da467010565968C7C62",
"amount": 200,
},
],
"nonce": 0,
"signature": "0x03c0e67cdc612bf1c0a690346805c5f461fbc0a8fe3041b4849c9ddbc939553a53997dfb6578200192e071618d9f054ae68513f134206149acf70ff04cea02931c",
},
"isManuallyEdited": False,
}
await sio.emit("allocate", data)

await sio.wait()


# Emit events
async def emit_event(event_name, data):
await sio.emit(event_name, data)


# Run the client
if __name__ == "__main__":
asyncio.run(main())
54 changes: 36 additions & 18 deletions backend/startup.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
# !!! IMPORTANT: DO NOT REARRANGE IMPORTS IN THIS FILE !!!
# The eventlet monkey patch needs to be applied before importing the Flask application for the following reasons:
# 1. Enabling Asynchronous I/O: The monkey patch is required to activate eventlet’s asynchronous and non-blocking I/O capabilities.
# Without this patch, the app's I/O requests might be blocked, which is not desirable for our API's performance.
# 2. Import Order Significance: The monkey patch must be applied before importing the Flask application to ensure that the app utilizes
# the asynchronous versions of standard library modules that have been patched by eventlet. If not done in this order, we might experience issues similar to
# what is reported in the following eventlet issue: https://github.com/eventlet/eventlet/issues/371
# This comment provides additional insight and helped resolve our specific problem: https://github.com/eventlet/eventlet/issues/371#issuecomment-779967181
# 3. Issue with dnspython: If dnspython is present in the environment, eventlet monkeypatches socket.getaddrinfo(),
# which breaks dns functionality. By setting the EVENTLET_NO_GREENDNS environment variable before importing eventlet,
# we prevent this monkeypatching

import os
from fastapi import Request
from fastapi.middleware.wsgi import WSGIMiddleware


os.environ["EVENTLET_NO_GREENDNS"] = "yes"
import eventlet # noqa
from starlette.middleware.base import BaseHTTPMiddleware

eventlet.monkey_patch()

if os.getenv("SENTRY_DSN"):
import sentry_sdk
Expand Down Expand Up @@ -54,13 +43,42 @@ def sentry_before_send(event, hint):
from app import create_app # noqa
from app.extensions import db # noqa

app = create_app()
# Create Flask app
flask_app = create_app()


@app.teardown_request
@flask_app.teardown_request
def teardown_session(*args, **kwargs):
db.session.remove()


from v2.main import fastapi_app # noqa

# Mount Flask app under a sub-path
fastapi_app.mount("/flask", WSGIMiddleware(flask_app))


# Middleware to check if the path exists in FastAPI
class PathCheckMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
path = request.url.path
# Check if the path exists in FastAPI routes
for route in fastapi_app.routes:
if path == route.path:
# If path exists, proceed with the request
return await call_next(request)
# If path does not exist, modify the request to forward to the Flask app
if path.startswith("/flask"):
return await call_next(request)
request.scope["path"] = "/flask" + path # Adjust the path as needed
response = await call_next(request)
return response


fastapi_app.add_middleware(PathCheckMiddleware)


if __name__ == "__main__":
eventlet.wsgi.server(eventlet.listen(("0.0.0.0", 5000)), app, log=app.logger)
import uvicorn

uvicorn.run(fastapi_app, host="0.0.0.0", port=5000)
Empty file added backend/v2/__init__.py
Empty file.
Empty file.
55 changes: 55 additions & 0 deletions backend/v2/allocations/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from typing import Annotated

from fastapi import Depends
from pydantic import Field
from pydantic_settings import BaseSettings
from v2.projects.services import EstimatedProjectMatchedRewards
from v2.epochs.dependencies import get_epochs_subgraph
from v2.epochs.subgraphs import EpochsSubgraph
from v2.projects.contracts import ProjectsContracts
from v2.projects.depdendencies import (
get_estimated_project_matched_rewards,
get_projects_contracts,
)
from v2.uniqueness_quotients.dependencies import get_uq_score_getter
from v2.uniqueness_quotients.services import UQScoreGetter
from v2.core.dependencies import AsyncDbSession

from .services import Allocations
from .validators import SignatureVerifier


class SignatureVerifierSettings(BaseSettings):
chain_id: int = Field(
default=11155111,
description="The chain id to use for the signature verification.",
)


def get_signature_verifier(
session: AsyncDbSession,
epochs_subgraph: Annotated[EpochsSubgraph, Depends(get_epochs_subgraph)],
projects_contracts: Annotated[ProjectsContracts, Depends(get_projects_contracts)],
settings: Annotated[SignatureVerifierSettings, Depends(SignatureVerifierSettings)],
) -> SignatureVerifier:
return SignatureVerifier(
session, epochs_subgraph, projects_contracts, settings.chain_id
)


def get_allocations(
session: AsyncDbSession,
signature_verifier: SignatureVerifier,
uq_score_getter: Annotated[UQScoreGetter, Depends(get_uq_score_getter)],
projects: Annotated[ProjectsContracts, Depends(get_projects_contracts)],
estimated_project_matched_rewards: Annotated[
EstimatedProjectMatchedRewards, Depends(get_estimated_project_matched_rewards)
],
) -> Allocations:
return Allocations(
session,
signature_verifier,
uq_score_getter,
projects,
estimated_project_matched_rewards,
)
Loading