Skip to content

Commit 945ca4f

Browse files
introudce chatbot client skeleton
1 parent 7aa3ac8 commit 945ca4f

File tree

4 files changed

+155
-0
lines changed

4 files changed

+155
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# mypy: disable-error-code=truthy-function
2+
from ._client import ChatbotQuestionCreate, ChatbotRestClient, get_chatbot_rest_client
3+
4+
__all__ = [
5+
"get_chatbot_rest_client",
6+
"ChatbotQuestionCreate",
7+
"ChatbotRestClient",
8+
]
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Interface to communicate with Fogbugz API
2+
3+
- Simple client to create cases in Fogbugz
4+
"""
5+
6+
import logging
7+
from typing import Any, Final
8+
from urllib.parse import urljoin
9+
10+
import httpx
11+
from aiohttp import web
12+
from pydantic import AnyUrl, BaseModel, Field
13+
from servicelib.aiohttp import status
14+
from tenacity import (
15+
retry,
16+
retry_if_exception_type,
17+
retry_if_result,
18+
stop_after_attempt,
19+
wait_exponential,
20+
)
21+
22+
from .settings import get_plugin_settings
23+
24+
_logger = logging.getLogger(__name__)
25+
26+
_JSON_CONTENT_TYPE = "application/json"
27+
_UNKNOWN_ERROR_MESSAGE = "Unknown error occurred"
28+
29+
30+
class ChatbotQuestionCreate(BaseModel):
31+
fogbugz_project_id: int = Field(description="Project ID in Fogbugz")
32+
title: str = Field(description="Case title")
33+
description: str = Field(description="Case description/first comment")
34+
35+
36+
def _should_retry(response: httpx.Response | None) -> bool:
37+
if response is None:
38+
return True
39+
return (
40+
response.status_code >= status.HTTP_500_INTERNAL_SERVER_ERROR
41+
or response.status_code == status.HTTP_429_TOO_MANY_REQUESTS
42+
)
43+
44+
45+
class ChatbotRestClient:
46+
def __init__(self, host: AnyUrl, port: int) -> None:
47+
self._client = httpx.AsyncClient()
48+
self.host = host
49+
self.port = port
50+
self._base_url = f"{self.host}:{self.port}"
51+
52+
async def get_settings(self) -> dict[str, Any]:
53+
"""Fetches chatbot settings"""
54+
url = urljoin(f"{self._base_url}", "/v1/chat/settings")
55+
56+
@retry(
57+
retry=(
58+
retry_if_result(_should_retry)
59+
| retry_if_exception_type(
60+
(
61+
httpx.ConnectError,
62+
httpx.TimeoutException,
63+
httpx.NetworkError,
64+
httpx.ProtocolError,
65+
)
66+
)
67+
),
68+
stop=stop_after_attempt(3),
69+
wait=wait_exponential(multiplier=1, min=1, max=10),
70+
reraise=True,
71+
)
72+
async def _request() -> httpx.Response:
73+
return await self._client.get(url)
74+
75+
try:
76+
response = await _request()
77+
response.raise_for_status()
78+
response_data: dict[str, Any] = response.json()
79+
return response_data
80+
except Exception:
81+
_logger.error( # noqa: TRY400
82+
"Failed to fetch chatbot settings from %s", url
83+
)
84+
raise
85+
86+
async def __aenter__(self):
87+
"""Async context manager entry"""
88+
return self
89+
90+
async def __aexit__(self, exc_type, exc_val, exc_tb):
91+
"""Async context manager exit - cleanup client"""
92+
await self._client.aclose()
93+
94+
95+
_APPKEY: Final = web.AppKey(ChatbotRestClient.__name__, ChatbotRestClient)
96+
97+
98+
async def setup_chatbot_rest_client(app: web.Application) -> None:
99+
settings = get_plugin_settings(app)
100+
101+
client = ChatbotRestClient(host=settings.CHATBOT_HOST, port=settings.CHATBOT_PORT)
102+
103+
app[_APPKEY] = client
104+
105+
106+
def get_chatbot_rest_client(app: web.Application) -> ChatbotRestClient:
107+
app_key: ChatbotRestClient = app[_APPKEY]
108+
return app_key
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""tags management subsystem"""
2+
3+
import logging
4+
5+
from aiohttp import web
6+
7+
from ..application_setup import ModuleCategory, app_setup_func
8+
from ..products.plugin import setup_products
9+
from ._client import setup_chatbot_rest_client
10+
11+
_logger = logging.getLogger(__name__)
12+
13+
14+
@app_setup_func(
15+
__name__,
16+
ModuleCategory.ADDON,
17+
settings_name="WEBSERVER_CHATBOT",
18+
logger=_logger,
19+
)
20+
def setup_chatbot(app: web.Application):
21+
setup_products(app)
22+
app.on_startup.append(setup_chatbot_rest_client)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from aiohttp import web
2+
from pydantic import AnyUrl
3+
from settings_library.base import BaseCustomSettings
4+
5+
from ..application_keys import APP_SETTINGS_APPKEY
6+
7+
8+
class ChatbotSettings(BaseCustomSettings):
9+
CHATBOT_HOST: AnyUrl
10+
CHATBOT_PORT: int
11+
12+
13+
def get_plugin_settings(app: web.Application) -> ChatbotSettings:
14+
settings = app[APP_SETTINGS_APPKEY].WEBSERVER_CHATBOT
15+
assert settings, "plugin.setup_chatbot not called?" # nosec
16+
assert isinstance(settings, ChatbotSettings) # nosec
17+
return settings

0 commit comments

Comments
 (0)