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

Basic endpoint implementation #255

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
22 changes: 16 additions & 6 deletions nextcore/common/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from __future__ import annotations

from asyncio import CancelledError, Future, create_task
from asyncio import CancelledError, Future, Task, create_task, gather
from collections import defaultdict
from logging import getLogger
from typing import TYPE_CHECKING, Generic, Hashable, TypeVar, cast, overload
Expand Down Expand Up @@ -423,7 +423,7 @@ async def wait_for(
return result # type: ignore [return-value]

# Dispatching
async def dispatch(self, event_name: EventNameT, *args: Any) -> None:
async def dispatch(self, event_name: EventNameT, *args: Any, wait: bool = False) -> None:
"""Dispatch a event

**Example usage:**
Expand All @@ -438,25 +438,35 @@ async def dispatch(self, event_name: EventNameT, *args: Any) -> None:
The event name to dispatch to.
args:
The event arguments. This will be passed to the listeners.
wait:
Wait for all listeners to complete.
"""
logger.debug("Dispatching event %s", event_name)

tasks: list[Task[None]] = []

# Event handlers
# Tasks are used here as some event handler/check might take a long time.
for handler in self._global_event_handlers:
logger.debug("Dispatching to a global handler")
create_task(self._run_global_event_handler(handler, event_name, *args))
tasks.append(create_task(self._run_global_event_handler(handler, event_name, *args)))
for handler in self._event_handlers.get(event_name, []):
logger.debug("Dispatching to a local handler")
create_task(self._run_event_handler(handler, event_name, *args))
tasks.append(create_task(self._run_event_handler(handler, event_name, *args)))

# Wait for handlers
for check, future in self._wait_for_handlers.get(event_name, []):
logger.debug("Dispatching to a wait_for handler")
create_task(self._run_wait_for_handler(check, future, event_name, *args))
tasks.append(create_task(self._run_wait_for_handler(check, future, event_name, *args)))
for check, future in self._global_wait_for_handlers:
logger.debug("Dispatching to a global wait_for handler")
create_task(self._run_global_wait_for_handler(check, future, event_name, *args))
tasks.append(create_task(self._run_global_wait_for_handler(check, future, event_name, *args)))

# Optional waiting
logger.debug("Dispatching via %s tasks", len(tasks))

if wait:
await gather(*tasks)

async def _run_event_handler(self, callback: EventCallback, event_name: EventNameT, *args: Any) -> None:
"""Run event with exception handlers"""
Expand Down
Empty file added nextcore/endpoint/__init__.py
Empty file.
44 changes: 44 additions & 0 deletions nextcore/endpoint/adapters/abc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# The MIT License (MIT)
# Copyright (c) 2021-present tag-epic
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from ..controller import InteractionController

class AbstractAdapter(ABC):
@abstractmethod
def set_interaction_controller(self, controller: InteractionController | None) -> None:
"""Set the interaction controller

This should register the controller and all events relating to interactions should be passed to the controller.

Parameters
----------
controller:
The controller to set. If this is :data:`None` remove the controller.

If a controller is already set, remove it.
"""
...
94 changes: 94 additions & 0 deletions nextcore/endpoint/adapters/aiohttp/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# The MIT License (MIT)
# Copyright (c) 2021-present tag-epic
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from __future__ import annotations
from typing import TYPE_CHECKING
from aiohttp.web_app import Application
from logging import getLogger

from aiohttp.web_response import json_response, Response

from ...interactions.request import InteractionRequest
from ..abc import AbstractAdapter

if TYPE_CHECKING:
from ...interactions.controller import InteractionController
from aiohttp.web_request import Request
from aiohttp.web_response import StreamResponse
from typing import Final

logger = getLogger(__name__)

class AIOHTTPAdapter(AbstractAdapter):
"""The AIOHTTP endpoint adapter
"""

def __init__(self) -> None:
self.app: Application = Application()

# Controllers
self._interaction_controller: InteractionController | None = None

self._add_routes()

def _add_routes(self):
"""This is automatically called by :meth:`__init__`!

The point of this being a seperate function is that it can be overriden by a sub-class.

.. note::
This counts as the public API, so any major changes to this will require a major version bump.
"""

self.app.router.add_post("/interaction", self.handle_interaction_request)

# Controller registrations
def set_interaction_controller(self, controller: InteractionController | None) -> None:
"""Set the interaction controller

This should register the controller and all events relating to interactions should be passed to the controller.

Parameters
----------
controller:
The controller to set. If this is :data:`None` remove the controller.

If a controller is already set, remove it.
"""
self._interaction_controller = controller

async def handle_interaction_request(self, request: Request) -> StreamResponse:
if self._interaction_controller is None:
return json_response({"detail": "No interaction controller registered"})

raw_body = await request.read()

try:
body = raw_body.decode()
except ValueError:
return json_response({"detail": "Request body is not UTF-8 encoded"}, status=400)

interaction_request = InteractionRequest(request.headers, body)

interaction_response = await self._interaction_controller.handle_interaction_request(interaction_request)

return Response(body=interaction_response.body, status=interaction_response.status_code, headers={"Content-Type": "application/json"})

42 changes: 42 additions & 0 deletions nextcore/endpoint/adapters/fastapi/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# The MIT License (MIT)
# Copyright (c) 2021-present tag-epic
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from __future__ import annotations

from fastapi import FastAPI
from typing import TYPE_CHECKING
from .models.interactions.error import Error as ErrorModel

if TYPE_CHECKING:
from typing import Final
from fastapi import Request, Response

class FastAPIAdapter:
"""The FastAPI endpoint adapter.
"""
def __init__(self) -> None:
self.app: Final[FastAPI] = FastAPI()

# Register handlers
self.app.add_api_route("/interaction", self.on_interaction_request, responses={200: ErrorModel})

async def on_interaction_request(self, request: Request) -> Response:
...
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from pydantic import BaseModel

class Error(BaseModel):
detail: str
21 changes: 21 additions & 0 deletions nextcore/endpoint/adapters/openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# The MIT License (MIT)
# Copyright (c) 2021-present tag-epic
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

20 changes: 20 additions & 0 deletions nextcore/endpoint/interactions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# The MIT License (MIT)
# Copyright (c) 2021-present tag-epic
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
Loading