Skip to content

Commit

Permalink
feat💥: revamp restful #2
Browse files Browse the repository at this point in the history
  • Loading branch information
Damego authored Aug 10, 2023
2 parents dd084e5 + 8ee27a1 commit 30003b4
Show file tree
Hide file tree
Showing 12 changed files with 279 additions and 198 deletions.
43 changes: 26 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,40 @@ A library for interactions.py allowing runtime API structures

## Installation

Using pip:
Using pip:
`pip install interactions-restful`

Using poetry:
Using poetry:
`poetry add interactions-restful`

Don't forget to specify backend you want to use:
- flask `pip install interactions-restful[flask]`
- fastapi `pip install interactions-restful[fastapi]`
- [FastAPI](https://fastapi.tiangolo.com/): `pip install interactions-restful[fastapi]`
- [Quart](https://pgjones.gitlab.io/quart/index.html): `pip install interactions-restful[quart]`

## Simple example
Also make sure to install an ASGI server:
- [Uvicorn](https://www.uvicorn.org/): `pip install interactions-restful[uvicorn]`
- [Hypercorn](https://pgjones.gitlab.io/hypercorn/): `pip install interactions-restful[hypercorn]`
- [Daphne](https://github.com/django/daphne): `pip install interactions-restful[daphne]`

You can also install both your backend and ASGI server at once, for example
`pip install interactions-restful[fastapi,uvicorn]`

## Simple (FastAPI) example

### Main file
- `main.py`

```python
import interactions
from interactions_restful import setup
from interactions_restful.backends.fast_api import FastAPI
from fastapi import FastAPI
from interactions_restful.backends.fastapi import FastAPIHandler

client = interactions.Client()
app = FastAPI()

setup(client, FastAPI, "127.0.0.1", 5000)
client = interactions.Client(token="token")
FastAPIHandler(client, app)

client.load_extension("api")

client.start("token")
```

### Extension file
Expand All @@ -44,17 +52,18 @@ class MyAPI(interactions.Extension):
@route("GET", "/")
def index(self):
return {"status": "Hello, i.py"}

@interactions.slash_command()
async def test_command(self, ctx):
await ctx.send("Hello, API")

```

## Backends
### Run

Currently, library support only flask and fastapi as a backend for building an api, but if you don't want to use them you can create own backend.
```bash
uvicorn main:app --reload
```

## Documentation
## Backends

[FastAPI documentation](https://fastapi.tiangolo.com/)
Currently, the library support only Quart and FastAPI as a backend for building an API, but if you don't want to use them you can create own backend.
13 changes: 6 additions & 7 deletions examples/bot.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import interactions
from interactions_restful import setup
from interactions_restful.backends.fast_api import FastAPI
from fastapi import FastAPI

from interactions_restful.backends.fastapi import FastAPIHandler

app = FastAPI()
client = interactions.Client(token="TOKEN")
FastAPIHandler(client, app)

client = interactions.Client()
setup(client, FastAPI, "localhost", 8000)
client.load_extension("exts.my_ext")


@interactions.listen()
async def on_startup():
print(f"Bot `{client.user.username}` started up")


client.start("TOKEN")
3 changes: 1 addition & 2 deletions interactions_restful/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
from .decorators import *
from .extension import *
from .decorators import * # noqa
94 changes: 72 additions & 22 deletions interactions_restful/abc.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,82 @@
from abc import ABC, abstractmethod
from typing import Callable, Coroutine
import asyncio
import inspect
from asyncio import Task
from typing import Any, Callable, Coroutine, Protocol

__all__ = ("BaseApi", "BaseRouter")
from interactions import CallbackObject, Client

from .manager import APIManager

class BaseRouter(ABC):
@abstractmethod
def add_endpoint_method(self, coro: Callable[..., Coroutine], endpoint: str, method: str, **kwargs):
pass
__all__ = ("BaseAPIHandler", "BaseRouterWrapper")


class BaseApi(ABC):
@abstractmethod
def __init__(self, host: str, port: int, **kwargs):
pass
class BaseRouterWrapper(Protocol):
def add_endpoint_method(
self, coro: Callable[..., Coroutine], endpoint: str, method: str, **kwargs
):
...

@abstractmethod
def add_endpoint_method(self, coro: Callable[..., Coroutine], endpoint: str, method: str, **kwargs):
pass

class BaseAPIHandler:
bot: Client
app: Any
task: Task | None
api_client: APIManager

def __init__(self, bot: Client, app: Any):
self.bot = bot
self.app = app
self.task = None
self.api_client = APIManager(self.bot, self)

# there's a problem deferring starting up the bot to an asgi server -
# sys.modules[__main__] will be replaced with the asgi server's module
# this means any files that were in the file where the bot is will be lost

# we can't actually fully work around this - not perfectly, anyway
# what we can somewhat rely on is that people are using the api wrappers
# in their main file - you could place it somewhere else, but why would
# you want to?
# regardless, we exploit this by getting the module of the caller of the
# api wrapper - in this, we have to go two calls back because the subclasses
# count as a call
# we can then use that module as the module to get the commands from during startup
stack = inspect.stack()
self.__original_module = inspect.getmodule(stack[2][0])

@staticmethod
@abstractmethod
def create_router(**kwargs) -> BaseRouter:
pass
def create_router(**kwargs) -> BaseRouterWrapper:
...

def add_router(self, router: BaseRouterWrapper):
...

def remove_router(self, router: BaseRouterWrapper):
...

def add_endpoint_method(
self, coro: Callable[..., Coroutine], endpoint: str, method: str, **kwargs
):
...

async def startup(self):
commands = inspect.getmembers(
self.__original_module, lambda x: isinstance(x, CallbackObject)
)
for command in commands:
self.bot.add_command(command[1])

route_callables = inspect.getmembers(
self.__original_module, predicate=lambda x: getattr(x, "__api__", False)
)
for callable in route_callables:
callback = callable[1]
data: dict = callback.__api__
self.add_endpoint_method(callback, data["endpoint"], data["method"], **data["kwargs"])

def add_router(self, router: BaseRouter):
pass
self.task = asyncio.create_task(self.bot.astart())

@abstractmethod
async def run(self):
pass
async def shutdown(self):
await self.bot.stop()
if self.task:
self.task.cancel()
50 changes: 0 additions & 50 deletions interactions_restful/backends/fast_api.py

This file was deleted.

48 changes: 48 additions & 0 deletions interactions_restful/backends/fastapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import Callable, Coroutine

from fastapi import APIRouter, FastAPI
from interactions import Client

from ..abc import BaseAPIHandler, BaseRouterWrapper

__all__ = ("FastAPIHandler",)


class FastAPIRouterWrapper(BaseRouterWrapper):
def __init__(self, **kwargs):
kwargs.pop("name") # neccessary only for quart
self.api_router = APIRouter(**kwargs)

def add_endpoint_method(
self, coro: Callable[..., Coroutine], endpoint: str, method: str, **kwargs
):
self.api_router.add_api_route(endpoint, coro, methods=[method], **kwargs)


class FastAPIHandler(BaseAPIHandler):
app: FastAPI

def __init__(self, bot: Client, app: FastAPI):
super().__init__(bot, app)
self.app.add_event_handler("startup", self.startup)
self.app.add_event_handler("shutdown", self.shutdown)

@staticmethod
def create_router(**kwargs):
return FastAPIRouterWrapper(**kwargs)

def add_router(self, router: FastAPIRouterWrapper):
self.app.include_router(router.api_router)

def remove_router(self, router: FastAPIRouterWrapper):
paths_to_check = frozenset(
f"{router.api_router.prefix}{r.path}" for r in router.api_router.routes
)
for i, r in enumerate(self.app.router.routes):
if r.path in paths_to_check: # type: ignore
del self.app.router.routes[i]

def add_endpoint_method(
self, coro: Callable[..., Coroutine], endpoint: str, method: str, **kwargs
):
self.app.add_api_route(endpoint, coro, methods=[method], **kwargs)
37 changes: 0 additions & 37 deletions interactions_restful/backends/flask_api.py

This file was deleted.

Loading

0 comments on commit 30003b4

Please sign in to comment.