Skip to content

Commit e8d9f38

Browse files
authored
Merge pull request #26 from codex-team/feat/fastapi
feat: added fastapi integration
2 parents 60af829 + 8ae1250 commit e8d9f38

File tree

10 files changed

+279
-37
lines changed

10 files changed

+279
-37
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Python errors Catcher module for [Hawk.so](https://hawk.so).
66

77
Register an account and get a new project token.
88

9+
If you want to connect specific frameworks see [Flask integration](./docs/flask.md), [FastAPI integration](./docs/fastapi.md).
10+
911
### Install module
1012

1113
Install `hawkcatcher` from PyPI.
@@ -104,7 +106,7 @@ Parameters:
104106

105107
## Requirements
106108

107-
- Python \>= 3.5
109+
- Python \>= 3.9
108110
- requests
109111

110112
## Links

docs/fastapi.md

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Flask integration
2+
3+
This extension adds support for the [FastAPI](https://fastapi.tiangolo.com/) web framework.
4+
5+
## Installation
6+
7+
```bash
8+
pip install hawkcatcher[fastapi]
9+
```
10+
11+
import Catcher module to your project.
12+
13+
```python
14+
from hawkcatcher.modules.fastapi import HawkFastapi
15+
```
16+
17+
```python
18+
app = FastAPI()
19+
20+
hawk = HawkFastapi(
21+
'app_instance': app,
22+
'token': '1234567-abcd-8901-efgh-123456789012'
23+
)
24+
```
25+
26+
Now all global fastapi errors would be sent to Hawk.
27+
28+
### Try-except
29+
30+
If you want to catch errors in try-except blocks see [this](../README.md#try-except)
31+
32+
## Manual sending
33+
34+
You can send any error to Hawk. See [this](../README.md#manual-sending)
35+
36+
### Event context
37+
38+
See [this](../README.md#event-context)
39+
40+
### Affected user
41+
42+
See [this](../README.md#affected-user)
43+
44+
### Addons
45+
46+
When some event handled by FastAPI Catcher, it adds some addons to the event data for Hawk.
47+
48+
| name | type | description |
49+
| --------- | ---- | --------------- |
50+
| `url` | str | Request URL |
51+
| `method` | str | Request method |
52+
| `headers` | dict | Request headers |
53+
| `cookies` | dict | Request cookies |
54+
| `params` | dict | Request params |
55+
56+
## Init params
57+
58+
To init Hawk Catcher just pass a project token and FastAPI app instance.
59+
60+
```python
61+
app = FastAPI()
62+
63+
hawk = HawkFastapi(
64+
'app_instance': app,
65+
'token': '1234567-abcd-8901-efgh-123456789012'
66+
)
67+
```
68+
69+
### Additional params
70+
71+
If you need to use custom Hawk server then pass a dictionary with params.
72+
73+
```python
74+
hawk = HawkFastapi({
75+
'app_instance': app,
76+
'token': '1234567-abcd-8901-efgh-123456789012',
77+
'collector_endpoint': 'https://<id>.k1.hawk.so',
78+
})
79+
```
80+
81+
Parameters:
82+
83+
| name | type | required | description |
84+
| -------------------- | ------------------------- | ------------ | ------------------------------------------------------------------------------ |
85+
| `app_instance` | FastAPI | **required** | FastAPI app instance |
86+
| `token` | str | **required** | Your project's Integration Token |
87+
| `release` | str | optional | Release name for Suspected Commits feature |
88+
| `collector_endpoint` | string | optional | Collector endpoint for sending event to |
89+
| `context` | dict | optional | Additional context to be send with every event |
90+
| `before_send` | Callable[[dict], None] | optional | This Method allows you to filter any data you don't want sending to Hawk |
91+
| `set_user` | Callable[[Request], User] | optional | This Method allows you to set user for every request by fastapi request object |
92+
| `with_addons` | bool | optional | Add framework addons to event data |
93+
94+
## Requirements
95+
96+
See [this](../README.md#requirements)
97+
98+
And for fastapi you need:
99+
100+
- fastapi

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ name = "hawkcatcher"
88
authors = [{ name = "CodeX Team", email = "[email protected]" }]
99
description = "Python errors Catcher module for Hawk."
1010
readme = "README.md"
11-
requires-python = ">=3.5"
11+
requires-python = ">=3.9"
1212
classifiers = [
1313
"Intended Audience :: Developers",
1414
"Topic :: Software Development :: Bug Tracking",
@@ -19,6 +19,7 @@ classifiers = [
1919
]
2020
[project.optional-dependencies]
2121
flask = ["flask"]
22+
fastapi = ["starlette"]
2223
[tool.hatch.version]
2324
path = "src/hawkcatcher/__init__.py"
2425
[project.urls]

src/hawkcatcher/core.py

+24-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import hawkcatcher
1111
from hawkcatcher.errors import InvalidHawkToken
12-
from hawkcatcher.types import HawkCatcherSettings
12+
from hawkcatcher.types import HawkCatcherSettings, Addons, User
1313

1414

1515
class Hawk:
@@ -49,7 +49,7 @@ def get_params(settings) -> Union[HawkCatcherSettings, None]:
4949
'context': settings.get('context', None)
5050
}
5151

52-
def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, user=None, addons=None):
52+
def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, user=None):
5353
"""
5454
Catch, prepare and send error
5555
@@ -70,6 +70,7 @@ def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, us
7070
ex_message = traceback.format_exception_only(exc_cls, exc)[-1]
7171
ex_message = ex_message.strip()
7272
backtrace = tb and Hawk.parse_traceback(tb)
73+
addons = self._set_addons()
7374

7475
if not (type(context) is dict):
7576
context = {
@@ -108,7 +109,7 @@ def send_to_collector(self, event):
108109
except Exception as e:
109110
print('[Hawk] Can\'t send error cause of %s' % e)
110111

111-
def send(self, event: Exception = None, context=None, user=None, addons=None):
112+
def send(self, event: Exception = None, context=None, user=None):
112113
"""
113114
Method for manually send error to Hawk
114115
:param event: event to send
@@ -119,9 +120,27 @@ def send(self, event: Exception = None, context=None, user=None, addons=None):
119120
exc_cls, exc, tb = sys.exc_info()
120121

121122
if event is not None:
122-
self.handler(type(event), event, tb, context, user, addons)
123+
self.handler(type(event), event, tb, context, user)
123124
else:
124-
self.handler(exc_cls, exc, tb, context, user, addons)
125+
self.handler(exc_cls, exc, tb, context, user)
126+
127+
def _set_addons(self) -> Union[Addons, None]:
128+
"""
129+
Set framework addons to send with error
130+
"""
131+
return None
132+
133+
def _set_user(self, request) -> Union[User, None]:
134+
"""
135+
Set user information by set_user callback
136+
"""
137+
user = None
138+
139+
if self.params.get('set_user') is not None:
140+
user = self.params['set_user'](request)
141+
142+
return user
143+
125144

126145
@staticmethod
127146
def parse_traceback(tb):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from .fastapi import HawkFastapi
2+
from .types import HawkCatcherSettings
3+
from .types import FastapiSettings
4+
5+
hawk = HawkFastapi()
6+
7+
8+
def init(*args, **kwargs):
9+
hawk.init(*args, **kwargs)
10+
11+
12+
def send(*args, **kwargs):
13+
hawk.send(*args, **kwargs)
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from hawkcatcher.types import HawkCatcherSettings
2+
from ...core import Hawk
3+
from hawkcatcher.modules.fastapi.types import FastapiSettings, FastapiAddons
4+
from starlette.types import ASGIApp, Receive, Scope, Send
5+
from starlette.requests import Request
6+
from starlette.middleware.base import BaseHTTPMiddleware
7+
from typing import Union
8+
from hawkcatcher.errors import ModuleError
9+
import asyncio
10+
from contextvars import ContextVar
11+
from fastapi import Request
12+
import asyncio
13+
14+
# Variable for saving current request, work with async tasks
15+
current_request: ContextVar[Union[Request, None]] = ContextVar("current_request", default=None)
16+
17+
18+
# class for catching errors in fastapi app
19+
class HawkFastapi(Hawk):
20+
params: FastapiSettings = {}
21+
22+
def init(self, settings: Union[str, FastapiSettings] = None):
23+
self.params = self.get_params(settings)
24+
25+
if self.params.get('app_instance') is None:
26+
raise ModuleError('Fastapi app instance not passed to HawkFastapi')
27+
28+
self.params.get('app_instance').add_middleware(self._get_starlette_middleware())
29+
30+
def _get_starlette_middleware(self):
31+
"""
32+
Create middleware for starlette to identify request exception and storing current request for manual sending
33+
"""
34+
35+
# Create method to use it in middleware class with Hawk class context
36+
def send_func(err):
37+
return self.send(err)
38+
39+
class StarletteMiddleware:
40+
def __init__(self, app: ASGIApp):
41+
self.app = app
42+
43+
async def __call__(self, scope: Scope, receive: Receive, send: Send):
44+
if scope["type"] == "http":
45+
request = Request(scope, receive, send)
46+
current_request.set(request)
47+
try:
48+
await self.app(scope, receive, send)
49+
except Exception as err:
50+
return send_func(err)
51+
else:
52+
await self.app(scope, receive, send)
53+
return None
54+
55+
return StarletteMiddleware
56+
57+
def send(self, event: Exception = None, context=None, user=None):
58+
"""
59+
Method for manually send error to Hawk, make it async for starlette
60+
:param exception: exception
61+
:param context: additional context to send with error
62+
:param user: user information who faced with that event
63+
"""
64+
65+
request = current_request.get()
66+
67+
if user is None and request is not None:
68+
user = self._set_user(request)
69+
70+
return super().send(event, context, user)
71+
72+
def _set_addons(self) -> Union[FastapiAddons, None]:
73+
request = current_request.get()
74+
75+
if request is None:
76+
return None
77+
78+
return {
79+
'fastapi': {
80+
'url': str(request.url),
81+
'method': request.method,
82+
'headers': dict(request.headers),
83+
'cookies': dict(request.cookies),
84+
'params': dict(request.query_params)
85+
}
86+
}
87+
88+
@staticmethod
89+
def get_params(settings) -> FastapiSettings | None:
90+
hawk_params = Hawk.get_params(settings)
91+
92+
if hawk_params is None:
93+
return None
94+
95+
return {
96+
**hawk_params,
97+
'app_instance': settings.get('app_instance'),
98+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from hawkcatcher.types import HawkCatcherSettings, User, Addons
2+
from typing import Callable, TypedDict
3+
from starlette.applications import Starlette
4+
from fastapi import Request
5+
6+
class FastapiAddons(TypedDict):
7+
url: str # url of request
8+
method: str # method of request
9+
headers: dict # headers of request
10+
cookies: dict # cookies of request
11+
params: dict # request params
12+
13+
class Addons(Addons):
14+
fastapi: FastapiAddons
15+
16+
class FastapiSettings(HawkCatcherSettings[Request]):
17+
"""Settings for Fastapi catcher for errors tracking"""
18+
19+
app_instance: Starlette # Fastapi app instance to add catching

0 commit comments

Comments
 (0)