Skip to content

Commit

Permalink
0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
yuvalherziger committed Dec 31, 2021
1 parent b340e0a commit 5126a23
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 5 deletions.
5 changes: 1 addition & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,12 @@ PYLINT = $(POETRY_RUN) pylint
PYTEST = $(POETRY_RUN) pytest
BANDIT = $(POETRY_RUN) bandit
PYTEST_TESTS := tests
CI_TARGETS := lint test
CI_TARGETS := pybandit pylint test


.PHONY: ci
ci: $(CI_TARGETS)

.PHONY: lint
lint: lint/py

.PHONY: test
test: test/py

Expand Down
187 changes: 187 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,191 @@

<div align="center">
<a href="https://github.com/yuvalherziger/aiohttp-catcher/actions?query=workflow%3ACI"><img alt="CI Job" src="https://github.com/yuvalherziger/aiohttp-catcher/workflows/CI/badge.svg"></a>
<a href="https://pypi.org/project/aiohttp-catcher"><img src="https://badge.fury.io/py/aiohttp-catcher.svg" alt="PyPI version" height="18"></a>
</div>

aiohttp-catcher is a centralized error handler for [aiohttp servers](https://docs.aiohttp.org/en/stable/web.html).
It enables consistant error handlling across your web server or API, so your code can raise Python exceptions that
will be handled however you want them to.

***

- [Quickstart](#quickstart)
- [Key Features](#key-features)
* [Return a Constant](#return-a-constant)
* [Stringify the Exception](#stringify-the-exception)
* [Callables and Awaitables](#callables-and-awaitables)
* [Handle Several Exceptions Similarly](#handle-several-exceptions-similarly)
* [Scenarios as Dictionaries](#scenarios-as-dictionaries)
- [Development](#development)

***

## Quickstart

```python
from aiohttp import web
from aiohttp_catcher import catch, Catcher

async def hello(request):
division = 1 / 0
return web.Response(text=f"1 / 0 = {division}")


async def main():
# Add a catcher:
catcher = Catcher()

# Register error-handling scenarios:
await catcher.add_scenario(
catch(ZeroDivisionError).with_status_code(400).and_return("Zero division makes zero sense")
)

# Register your catcher as an aiohttp middleware:
app = web.Application(middlewares=[catcher.middleware])
app.add_routes([web.get("/divide-by-zero", hello)])
web.run_app(app)
```

Making a request to `/divide-by-zero` will return a 400 status code with the following body:
```json
{"code": 400, "message": "Zero division makes zero sense"}
```

***

## Key Features

### Return a Constant

In case you want some exceptions to return a constant message across your application, you can do
so by using the `.and_return("some value")` method:

```python
await catcher.add_scenario(
catch(ZeroDivisionError).with_status_code(400).and_return("Zero division makes zero sense")
)
```

***

### Stringify the Exception

In some cases, you would want to return a stringified version of your exception, should it entail
user-friendly information.

```
class EntityNotFound(Exception):
def __init__(self, entity_id, *args, **kwargs):
super(EntityNotFound, self).__init__(*args, **kwargs)
self.entity_id = entity_id
def __str__(self):
return f"Entity {self.entity_id} could not be found"
@routes.get("/user/{user_id}")
async def get_user(request):
user_id = request.match_info.get("user_id")
if user_id not in user_db:
raise EntityNotFound(entity_id=user_id)
return user_db[user_id]
# Your catcher can be directed to stringify particular exceptions:
await catcher.add_scenario(
catch(EntityNotFound).with_status_code(404).and_stringify()
)
```

***

### Callables and Awaitables

In some cases, you'd want the message returned by your server for some exceptions to call a custom
function. This function can either be a synchronous function or an awaitable one. It should expect
a single argument, which is the exception being raised:

```python
# Can be a synchronous function as well:
async def write_message(exc: Exception):
return "Whoops"

await catcher.add_scenarios(
catch(MyCustomException2).with_status_code(401).and_call(write_message),
catch(MyCustomException2).with_status_code(403).and_call(lambda exc: str(exc))
)

```

***

### Handle Several Exceptions Similarly

You can handle several exceptions in the same manner by adding them to the same scenario:

```python
await catcher.add_scenario(
catch(
MyCustomException1,
MyCustomException2,
MyCustomException3
).with_status_code(418).and_return("User-friendly error message")
)
```

***

### Scenarios as Dictionaries

You can register your scenarios as dictionaries as well:

```python
await catcher.add_scenarios(
{
"exceptions": [ZeroDivisionError],
"constant": "Zero division makes zero sense",
"status_code": 400,
},
{
"exceptions": [EntityNotFound],
"stringify_exception": True,
"status_code": 404,
},
{
"exceptions": [IndexError],
"func": lambda exc: f"Out of bound: {str(exc)}",
"status_code": 418,
},
)
```

***

## Development

Contributions are warmly welcomed. Before submitting your PR, please run the tests using the following Make target:

```bash
make ci
```

Alternatively, you can run each test suite separately:

1. Unit tests:

```bash
make test/py
```

2. Linting with pylint:

```bash
make pylint
```

3. Static security checks with bandit:

```bash
make pybandit
```
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
[tool.poetry]
name = "aiohttp-catcher"
version = "0.1.0-rc.0"
version = "0.1.0"
description = "A centralized error handler for aiohttp servers"
authors = ["Yuvi Herziger <[email protected]>"]
license = "MIT"
readme = "README.md"
homepage = "https://github.com/yuvalherziger/aiohttp-catcher"
repository = "https://github.com/yuvalherziger/aiohttp-catcher"
keywords = ["python", "python3", "aiohttp", "error-handling"]

[tool.poetry.dependencies]
python = "^3.8"
Expand Down

0 comments on commit 5126a23

Please sign in to comment.