From b8f221cecc3cc2701b01aa6b272059f844b01553 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Mon, 28 Oct 2024 03:48:57 +0300 Subject: [PATCH 1/6] feat: add flask support --- pyproject.toml | 2 +- src/hawkcatcher/__init__.py | 4 ++- src/hawkcatcher/errors.py | 3 ++ src/hawkcatcher/modules/flask.py | 60 ++++++++++++++++++++++++++++++++ src/hawkcatcher/modules/types.py | 9 +++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/hawkcatcher/modules/flask.py create mode 100644 src/hawkcatcher/modules/types.py diff --git a/pyproject.toml b/pyproject.toml index a3d6d0b..d1d2f30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] dynamic = ["version"] -dependencies = ["requests"] +dependencies = ["requests", "flask"] name = "hawkcatcher" authors = [{ name = "CodeX Team", email = "team@codex.so" }] description = "Python errors Catcher module for Hawk." diff --git a/src/hawkcatcher/__init__.py b/src/hawkcatcher/__init__.py index 41db312..cc47b13 100644 --- a/src/hawkcatcher/__init__.py +++ b/src/hawkcatcher/__init__.py @@ -2,6 +2,8 @@ from .core import Hawk from .types import HawkCatcherSettings +from .modules.types import FlaskSettings +from .modules.flask import HawkFlask hawk = Hawk() @@ -11,4 +13,4 @@ def init(*args, **kwargs): def send(*args, **kwargs): - hawk.send(*args, **kwargs) + hawk.send(*args, **kwargs) \ No newline at end of file diff --git a/src/hawkcatcher/errors.py b/src/hawkcatcher/errors.py index 1a62649..69a7458 100644 --- a/src/hawkcatcher/errors.py +++ b/src/hawkcatcher/errors.py @@ -1,2 +1,5 @@ class InvalidHawkToken(Exception): + pass + +class ModuleError(Exception): pass \ No newline at end of file diff --git a/src/hawkcatcher/modules/flask.py b/src/hawkcatcher/modules/flask.py new file mode 100644 index 0000000..df936e7 --- /dev/null +++ b/src/hawkcatcher/modules/flask.py @@ -0,0 +1,60 @@ +from ..core import Hawk +from typing import Union +from hawkcatcher.modules.types import FlaskSettings +from hawkcatcher.errors import ModuleError + +try: + from flask.signals import got_request_exception + from flask import Flask, request +except ImportError: + raise ModuleError("Flask is not installed") + +# class for catching errors in flask app +class HawkFlask(Hawk): + params: FlaskSettings = {} + + def init(self, settings: Union[str, FlaskSettings] = None) -> None: + self.params = self.get_params(settings) + got_request_exception.connect(self.handle_request_exception) + + @staticmethod + def get_params(settings) -> Union[FlaskSettings, None]: + hawk_params = Hawk.get_params(settings) + + if hawk_params is None: + return None + + return { + **hawk_params, + 'set_user': settings.get('set_user'), + 'with_request_data': settings.get('with_request_data', True) + } + + def handle_request_exception(self, sender: Flask, exception): + """ + Catch, prepare and send error + + :param sender: flask app + :param exception: exception + """ + ctx = {} + + if self.params.get('with_request_data') == True: + headers = dict(request.headers) + cookies = dict(request.cookies) + ctx = { + 'app': sender.name, + 'url': request.url, + 'method': request.method, + 'headers': headers, + 'cookies': cookies, + 'params': request.args, + 'form': request.form, + 'json': request.json + } + + if self.params.get('set_user') is not None: + ctx['user'] = self.params['set_user'](request) + + self.send(exception, ctx) + diff --git a/src/hawkcatcher/modules/types.py b/src/hawkcatcher/modules/types.py new file mode 100644 index 0000000..a4a905a --- /dev/null +++ b/src/hawkcatcher/modules/types.py @@ -0,0 +1,9 @@ +from hawkcatcher.types import HawkCatcherSettings +from typing import Callable +from flask import Request + +class FlaskSettings(HawkCatcherSettings): + """Settings for Flask catcher for errors tracking""" + + set_user: Callable[[Request], dict] # This hook allows you to identify user + with_request_data: bool = True # This parameter points if you want to send request data with error (cookies, headers, params, form, json) \ No newline at end of file From 72313682e01eac9b14670069882b0c8d18a9e4a6 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Tue, 29 Oct 2024 16:13:30 +0300 Subject: [PATCH 2/6] feat: add extra deps for flask, added addons and context to config --- pyproject.toml | 4 +- src/hawkcatcher/__init__.py | 3 +- src/hawkcatcher/core.py | 19 +++-- src/hawkcatcher/modules/flask.py | 60 -------------- src/hawkcatcher/modules/flask/__init__.py | 13 +++ src/hawkcatcher/modules/flask/flask.py | 97 +++++++++++++++++++++++ src/hawkcatcher/modules/flask/types.py | 22 +++++ src/hawkcatcher/modules/types.py | 9 --- src/hawkcatcher/types.py | 9 +++ 9 files changed, 159 insertions(+), 77 deletions(-) delete mode 100644 src/hawkcatcher/modules/flask.py create mode 100644 src/hawkcatcher/modules/flask/__init__.py create mode 100644 src/hawkcatcher/modules/flask/flask.py create mode 100644 src/hawkcatcher/modules/flask/types.py delete mode 100644 src/hawkcatcher/modules/types.py diff --git a/pyproject.toml b/pyproject.toml index d1d2f30..0f65325 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] dynamic = ["version"] -dependencies = ["requests", "flask"] +dependencies = ["requests"] name = "hawkcatcher" authors = [{ name = "CodeX Team", email = "team@codex.so" }] description = "Python errors Catcher module for Hawk." @@ -17,6 +17,8 @@ classifiers = [ "Environment :: Console", "Environment :: Web Environment", ] +[project.optional-dependencies] +flask = ["flask"] [tool.hatch.version] path = "src/hawkcatcher/__init__.py" [project.urls] diff --git a/src/hawkcatcher/__init__.py b/src/hawkcatcher/__init__.py index cc47b13..e3aa112 100644 --- a/src/hawkcatcher/__init__.py +++ b/src/hawkcatcher/__init__.py @@ -2,8 +2,7 @@ from .core import Hawk from .types import HawkCatcherSettings -from .modules.types import FlaskSettings -from .modules.flask import HawkFlask +from .modules.flask.types import FlaskSettings hawk = Hawk() diff --git a/src/hawkcatcher/core.py b/src/hawkcatcher/core.py index ab8425a..63954ff 100644 --- a/src/hawkcatcher/core.py +++ b/src/hawkcatcher/core.py @@ -46,9 +46,10 @@ def get_params(settings) -> Union[HawkCatcherSettings, None]: settings.get('token')), 'release': settings.get('release'), 'before_send': settings.get('before_send'), + 'context': settings.get('context', None) } - def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, user=None): + def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, user=None, addons=None): """ Catch, prepare and send error @@ -61,6 +62,10 @@ def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, us if not self.params: return + + # in case passed context is empty set default from config + if context is None: + context = self.params.get('context') ex_message = traceback.format_exception_only(exc_cls, exc)[-1] ex_message = ex_message.strip() @@ -71,6 +76,9 @@ def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, us 'value': context } + if addons is None: + addons = {} + event = { 'token': self.params['token'], 'catcherType': 'errors/python', @@ -81,7 +89,8 @@ def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, us 'release': self.params['release'], 'context': context, 'catcherVersion': hawkcatcher.__version__, - 'user': user + 'user': user, + 'addons': addons } } @@ -99,7 +108,7 @@ def send_to_collector(self, event): except Exception as e: print('[Hawk] Can\'t send error cause of %s' % e) - def send(self, event: Exception = None, context=None, user=None): + def send(self, event: Exception = None, context=None, user=None, addons=None): """ Method for manually send error to Hawk :param event: event to send @@ -110,9 +119,9 @@ def send(self, event: Exception = None, context=None, user=None): exc_cls, exc, tb = sys.exc_info() if event is not None: - self.handler(type(event), event, tb, context, user) + self.handler(type(event), event, tb, context, user, addons) else: - self.handler(exc_cls, exc, tb, context, user) + self.handler(exc_cls, exc, tb, context, user, addons) @staticmethod def parse_traceback(tb): diff --git a/src/hawkcatcher/modules/flask.py b/src/hawkcatcher/modules/flask.py deleted file mode 100644 index df936e7..0000000 --- a/src/hawkcatcher/modules/flask.py +++ /dev/null @@ -1,60 +0,0 @@ -from ..core import Hawk -from typing import Union -from hawkcatcher.modules.types import FlaskSettings -from hawkcatcher.errors import ModuleError - -try: - from flask.signals import got_request_exception - from flask import Flask, request -except ImportError: - raise ModuleError("Flask is not installed") - -# class for catching errors in flask app -class HawkFlask(Hawk): - params: FlaskSettings = {} - - def init(self, settings: Union[str, FlaskSettings] = None) -> None: - self.params = self.get_params(settings) - got_request_exception.connect(self.handle_request_exception) - - @staticmethod - def get_params(settings) -> Union[FlaskSettings, None]: - hawk_params = Hawk.get_params(settings) - - if hawk_params is None: - return None - - return { - **hawk_params, - 'set_user': settings.get('set_user'), - 'with_request_data': settings.get('with_request_data', True) - } - - def handle_request_exception(self, sender: Flask, exception): - """ - Catch, prepare and send error - - :param sender: flask app - :param exception: exception - """ - ctx = {} - - if self.params.get('with_request_data') == True: - headers = dict(request.headers) - cookies = dict(request.cookies) - ctx = { - 'app': sender.name, - 'url': request.url, - 'method': request.method, - 'headers': headers, - 'cookies': cookies, - 'params': request.args, - 'form': request.form, - 'json': request.json - } - - if self.params.get('set_user') is not None: - ctx['user'] = self.params['set_user'](request) - - self.send(exception, ctx) - diff --git a/src/hawkcatcher/modules/flask/__init__.py b/src/hawkcatcher/modules/flask/__init__.py new file mode 100644 index 0000000..d46f9ce --- /dev/null +++ b/src/hawkcatcher/modules/flask/__init__.py @@ -0,0 +1,13 @@ +from .flask import HawkFlask +from .types import HawkCatcherSettings +from .types import FlaskSettings + +hawk = HawkFlask() + + +def init(*args, **kwargs): + hawk.init(*args, **kwargs) + + +def send(*args, **kwargs): + hawk.send(*args, **kwargs) \ No newline at end of file diff --git a/src/hawkcatcher/modules/flask/flask.py b/src/hawkcatcher/modules/flask/flask.py new file mode 100644 index 0000000..ec37217 --- /dev/null +++ b/src/hawkcatcher/modules/flask/flask.py @@ -0,0 +1,97 @@ +from ...core import Hawk +from typing import Union +from hawkcatcher.modules.flask.types import FlaskSettings, User, Addons +from hawkcatcher.errors import ModuleError + +try: + from flask.signals import got_request_exception + from flask import Flask, request +except ImportError: + raise ModuleError("Flask is not installed") + +# class for catching errors in flask app +class HawkFlask(Hawk): + params: FlaskSettings = {} + + def init(self, settings: Union[str, FlaskSettings] = None) -> None: + self.params = self.get_params(settings) + got_request_exception.connect(self._handle_request_exception) + + @staticmethod + def get_params(settings) -> Union[FlaskSettings, None]: + hawk_params = Hawk.get_params(settings) + + if hawk_params is None: + return None + + return { + **hawk_params, + 'set_user': settings.get('set_user'), + 'with_addons': settings.get('with_addons', True) + } + + def send(self, exception, context=None, user=None, addons=None): + """ + Method for manually send error to Hawk + :param exception: exception + :param context: additional context to send with error + :param user: user information who faced with that event + """ + if addons is None: + addons = self._set_addons() + + if user is None: + user = self._set_user(request) + + super().send(exception, context, user, addons) + + def _handle_request_exception(self, sender: Flask, exception): + """ + Catch, prepare and send error + + :param sender: flask app + :param exception: exception + """ + addons = self._set_addons() + + user = self._set_user(request) + + ctx = self.params.get('context', None) + + self.send(exception, ctx, user, addons) + + def _set_addons(self) -> Union[Addons, None]: + """ + Set flask addons to send with error + """ + addons: Union[Addons, None] = None + + if self.params.get('with_addons') == True: + headers = dict(request.headers) + cookies = dict(request.cookies) + + addons = { + 'flask': { + 'url': request.url, + 'method': request.method, + 'headers': headers, + 'cookies': cookies, + 'params': request.args, + 'form': request.form, + 'json': request.json + } + } + + return addons + + def _set_user(self, request) -> Union[User, None]: + """ + Set user information by set_user callback + """ + user = None + + if self.params.get('set_user') is not None: + user = self.params['set_user'](request) + + return user + diff --git a/src/hawkcatcher/modules/flask/types.py b/src/hawkcatcher/modules/flask/types.py new file mode 100644 index 0000000..850f5bf --- /dev/null +++ b/src/hawkcatcher/modules/flask/types.py @@ -0,0 +1,22 @@ +from hawkcatcher.types import HawkCatcherSettings, User +from typing import Callable, TypedDict +from flask import Request + +class FlaskAddons(TypedDict): + app: str # name of flask app + url: str # url of request + method: str # method of request + headers: dict # headers of request + cookies: dict # cookies of request + params: dict # request params + form: dict # request form data + json: dict # request json data + +class Addons(TypedDict): + flask: FlaskAddons + +class FlaskSettings(HawkCatcherSettings): + """Settings for Flask catcher for errors tracking""" + + set_user: Callable[[Request], User] # This hook allows you to identify user + with_addons: bool = True # This parameter points if you want to send request data with error (cookies, headers, params, form, json) diff --git a/src/hawkcatcher/modules/types.py b/src/hawkcatcher/modules/types.py deleted file mode 100644 index a4a905a..0000000 --- a/src/hawkcatcher/modules/types.py +++ /dev/null @@ -1,9 +0,0 @@ -from hawkcatcher.types import HawkCatcherSettings -from typing import Callable -from flask import Request - -class FlaskSettings(HawkCatcherSettings): - """Settings for Flask catcher for errors tracking""" - - set_user: Callable[[Request], dict] # This hook allows you to identify user - with_request_data: bool = True # This parameter points if you want to send request data with error (cookies, headers, params, form, json) \ No newline at end of file diff --git a/src/hawkcatcher/types.py b/src/hawkcatcher/types.py index 5e1cfc8..38d5987 100644 --- a/src/hawkcatcher/types.py +++ b/src/hawkcatcher/types.py @@ -1,6 +1,14 @@ from typing import TypedDict, Callable +class User(TypedDict): + """User data for sending with event""" + + id: str # Internal user's identifier inside an app + name: str # User public name + image: str # User's public picture + url: str # URL for user's details page + class HawkCatcherSettings(TypedDict): """Settings for Hawk catcher for errors tracking""" @@ -8,3 +16,4 @@ class HawkCatcherSettings(TypedDict): collector_endpoint: str # Collector endpoint for sending event to release: str # Release name for Suspected Commits feature before_send: Callable[[dict], None] # This hook allows you to filter any data you don't want sending to Hawk + context: dict # Additional context to be send with event From 77f75b78d120afdf2594c362ac7a13873d7ebb63 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Tue, 29 Oct 2024 16:15:57 +0300 Subject: [PATCH 3/6] fix: removed __init__ changes --- src/hawkcatcher/modules/flask/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hawkcatcher/modules/flask/__init__.py b/src/hawkcatcher/modules/flask/__init__.py index d46f9ce..8943bb4 100644 --- a/src/hawkcatcher/modules/flask/__init__.py +++ b/src/hawkcatcher/modules/flask/__init__.py @@ -1,6 +1,5 @@ from .flask import HawkFlask from .types import HawkCatcherSettings -from .types import FlaskSettings hawk = HawkFlask() @@ -10,4 +9,4 @@ def init(*args, **kwargs): def send(*args, **kwargs): - hawk.send(*args, **kwargs) \ No newline at end of file + hawk.send(*args, **kwargs) From db054a9eeaabb1658fcdc34adaf25e3f09adeceb Mon Sep 17 00:00:00 2001 From: slaveeks Date: Tue, 29 Oct 2024 16:16:54 +0300 Subject: [PATCH 4/6] fix: removed __init__ changes --- src/hawkcatcher/__init__.py | 3 +-- src/hawkcatcher/modules/flask/__init__.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hawkcatcher/__init__.py b/src/hawkcatcher/__init__.py index e3aa112..41db312 100644 --- a/src/hawkcatcher/__init__.py +++ b/src/hawkcatcher/__init__.py @@ -2,7 +2,6 @@ from .core import Hawk from .types import HawkCatcherSettings -from .modules.flask.types import FlaskSettings hawk = Hawk() @@ -12,4 +11,4 @@ def init(*args, **kwargs): def send(*args, **kwargs): - hawk.send(*args, **kwargs) \ No newline at end of file + hawk.send(*args, **kwargs) diff --git a/src/hawkcatcher/modules/flask/__init__.py b/src/hawkcatcher/modules/flask/__init__.py index 8943bb4..dc5f682 100644 --- a/src/hawkcatcher/modules/flask/__init__.py +++ b/src/hawkcatcher/modules/flask/__init__.py @@ -1,5 +1,6 @@ from .flask import HawkFlask from .types import HawkCatcherSettings +from .types import FlaskSettings hawk = HawkFlask() From 0e25e4b3a60c8ff00ac8a90bd28ef3252c4d2d86 Mon Sep 17 00:00:00 2001 From: slaveeks Date: Sat, 2 Nov 2024 01:15:33 +0300 Subject: [PATCH 5/6] feat: added info about additional params --- README.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c5dccde..93a7474 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -Hawk Python Catcher -=========== +# Hawk Python Catcher Python errors Catcher module for [Hawk.so](https://hawk.so). -Usage ------ +## Usage Register an account and get a new project token. @@ -53,7 +51,6 @@ except: hawk.send(ValueError("error description")) ``` - ### Event context It is possible to pass additional event context for debugging purposes: @@ -76,8 +73,7 @@ except: hawk.send(ValueError("error description"), {"params": "value"}, {"id": 123}) ``` -Init params ------------ +## Init params To init Hawk Catcher just pass a project token. @@ -96,14 +92,22 @@ hawk = Hawk({ }) ``` -Requirements ------------- +Parameters: + +| name | type | required | description | +| -------------------- | ---------------------- | ------------ | ------------------------------------------------------------------------ | +| `token` | str | **required** | Your project's Integration Token | +| `release` | str | optional | Release name for Suspected Commits feature | +| `collector_endpoint` | string | optional | Collector endpoint for sending event to | +| `context` | dict | optional | Additional context to be send with every event | +| `before_send` | Callable[[dict], None] | optional | This Method allows you to filter any data you don't want sending to Hawk | + +## Requirements - Python \>= 3.5 - requests -Links ------ +## Links Repository: @@ -111,4 +115,4 @@ Report a bug: PyPI Package: -CodeX Team: +CodeX Team: From e85e1a4534cb1dcd870c7d60a1cc98248fd8d46d Mon Sep 17 00:00:00 2001 From: slaveeks Date: Sat, 2 Nov 2024 01:51:39 +0300 Subject: [PATCH 6/6] feat: added flask docs --- docs/flask.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/flask.md diff --git a/docs/flask.md b/docs/flask.md new file mode 100644 index 0000000..3b3dfd0 --- /dev/null +++ b/docs/flask.md @@ -0,0 +1,92 @@ +# Flask integration + +This extension adds support for the [Flask](http://flask.pocoo.org/) web framework. + +## Installation + +```bash +pip install hawkcatcher[flask] +``` + +import Catcher module to your project. + +```python +from hawkcatcher.modules.flask import HawkFlask +``` + +```python +hawk = HawkFlask( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9qZWN0SWQiOiI1ZTZmNWM3NzAzOWI0MDAwMjNmZDViODAiLCJpYXQiOjE1ODQzNTY0NzF9.t-5Gelx3MgHVBrxTsoMyPQAdQ6ufVbPsts9zZLW3gM8") +``` + +Now all global flask errors would be sent to Hawk. + +### Try-except + +If you want to catch errors in try-except blocks see [this](../README.md#try-except) + +## Manual sending + +You can send any error to Hawk. See [this](../README.md#manual-sending) + +### Event context + +See [this](../README.md#event-context) + +### Affected user + +See [this](../README.md#affected-user) + +### Addons + +When some event handled by Flask Catcher, it adds some addons to the event data for Hawk. + +| name | type | description | +| --------- | ---- | ----------------- | +| `url` | str | Request URL | +| `method` | str | Request method | +| `headers` | dict | Request headers | +| `cookies` | dict | Request cookies | +| `params` | dict | Request params | +| `form` | dict | Request form | +| `json` | dict | Request json data | + +## Init params + +To init Hawk Catcher just pass a project token. + +```python +hawk = HawkFlask('1234567-abcd-8901-efgh-123456789012') +``` + +### Additional params + +If you need to use custom Hawk server then pass a dictionary with params. + +```python +hawk = HawkFlask({ + 'token': '1234567-abcd-8901-efgh-123456789012', + 'collector_endpoint': 'https://.k1.hawk.so', +}) +``` + +Parameters: + +| name | type | required | description | +| -------------------- | ------------------------- | ------------ | ---------------------------------------------------------------------------- | +| `token` | str | **required** | Your project's Integration Token | +| `release` | str | optional | Release name for Suspected Commits feature | +| `collector_endpoint` | string | optional | Collector endpoint for sending event to | +| `context` | dict | optional | Additional context to be send with every event | +| `before_send` | Callable[[dict], None] | optional | This Method allows you to filter any data you don't want sending to Hawk | +| `set_user` | Callable[[Request], User] | optional | This Method allows you to set user for every request by flask request object | +| `with_addons` | bool | optional | Add framework addons to event data | + +## Requirements + +See [this](../README.md#requirements) + +And for flask you need: + +- Flask +- blinker