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

Add Flask support #4

Merged
merged 7 commits into from
Aug 17, 2023
Merged
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
7 changes: 6 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- name: Run Poetry check
run: poetry check
- name: Install dependencies
run: poetry install --no-interaction --with dev
run: poetry install --no-interaction --all-extras
- name: Run checks
run: poetry run make check
- name: Run tests and create coverage report
Expand All @@ -59,6 +59,11 @@ jobs:
- fastapi==0.99.1 starlette
- fastapi==0.90.1 starlette
- fastapi==0.87.0 starlette
- flask
- flask==2.3.2
- flask==2.2.5
- flask==2.1.3
- flask==2.0.3
steps:
- uses: actions/checkout@v3
- name: Load cached Poetry installation
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
notebooks/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
134 changes: 0 additions & 134 deletions apitally/client.py

This file was deleted.

Empty file added apitally/client/__init__.py
Empty file.
92 changes: 92 additions & 0 deletions apitally/client/asyncio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from __future__ import annotations

import asyncio
import logging
import sys
from typing import Any, Dict, Optional

import backoff
import httpx

from apitally.client.base import ApitallyClientBase, handle_retry_giveup


logger = logging.getLogger(__name__)
retry = backoff.on_exception(
backoff.expo,
httpx.HTTPError,
max_tries=3,
on_giveup=handle_retry_giveup,
raise_on_giveup=False,
)


class ApitallyClient(ApitallyClientBase):
def __init__(self, client_id: str, env: str, enable_keys: bool = False, sync_interval: float = 60) -> None:
super().__init__(client_id=client_id, env=env, enable_keys=enable_keys, sync_interval=sync_interval)
self._stop_sync_loop = False
self._sync_loop_task: Optional[asyncio.Task[Any]] = None

def get_http_client(self) -> httpx.AsyncClient:
return httpx.AsyncClient(base_url=self.hub_url, timeout=1)

def start_sync_loop(self) -> None:
self._stop_sync_loop = False
self._sync_loop_task = asyncio.create_task(self._run_sync_loop())

async def _run_sync_loop(self) -> None:
if self.enable_keys:
try:
async with self.get_http_client() as client:
await self.get_keys(client)
except Exception as e:
logger.exception(e)
while not self._stop_sync_loop:
try:
await asyncio.sleep(self.sync_interval)
async with self.get_http_client() as client:
await self.send_requests_data(client)
if self.enable_keys:
await self.get_keys(client)
except Exception as e: # pragma: no cover
logger.exception(e)

def stop_sync_loop(self) -> None:
self._stop_sync_loop = True

def send_app_info(self, app_info: Dict[str, Any]) -> None:
payload = self.get_info_payload(app_info)
asyncio.create_task(self._send_app_info(payload=payload))

async def send_requests_data(self, client: httpx.AsyncClient) -> None:
payload = self.get_requests_payload()
await self._send_requests_data(client, payload)

async def get_keys(self, client: httpx.AsyncClient) -> None:
if response_data := await self._get_keys(client): # Response data can be None if backoff gives up
self.handle_keys_response(response_data)
elif self.key_registry.salt is None:
logger.error("Initial Apitally key sync failed")
# Exit because the application will not be able to authenticate requests
sys.exit(1)

@retry
async def _send_app_info(self, payload: Dict[str, Any]) -> None:
async with self.get_http_client() as client:
response = await client.post(url="/info", json=payload, timeout=1)
if response.status_code == 404 and "Client ID" in response.text:
self.stop_sync_loop()
logger.error(f"Invalid Apitally client ID {self.client_id}")
else:
response.raise_for_status()

@retry
async def _send_requests_data(self, client: httpx.AsyncClient, payload: Dict[str, Any]) -> None:
response = await client.post(url="/requests", json=payload)
response.raise_for_status()

@retry
async def _get_keys(self, client: httpx.AsyncClient) -> Dict[str, Any]:
response = await client.get(url="/keys")
response.raise_for_status()
return response.json()
Loading