From eface504489b38a577aa8affe004b4b00b2b76f3 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 9 Jan 2021 16:19:24 +0000 Subject: [PATCH 1/6] point to the correct coverage report --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b760e66..a39f14d 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ [![PyPI version](https://badge.fury.io/py/aio-openapi.svg)](https://badge.fury.io/py/aio-openapi) [![Python versions](https://img.shields.io/pypi/pyversions/aio-openapi.svg)](https://pypi.org/project/aio-openapi) [![Build](https://github.com/quantmind/aio-openapi/workflows/build/badge.svg)](https://github.com/quantmind/aio-openapi/actions?query=workflow%3Abuild) -[![Coverage Status](https://coveralls.io/repos/github/quantmind/aio-openapi/badge.svg?branch=HEAD)](https://coveralls.io/github/quantmind/aio-openapi?branch=HEAD) +[![Coverage Status](https://coveralls.io/repos/github/quantmind/aio-openapi/badge.svg?branch=HEAD)](https://coveralls.io/github/quantmind/aio-openapi?branch=master) [![Documentation Status](https://readthedocs.org/projects/aio-openapi/badge/?version=latest)](https://aio-openapi.readthedocs.io/en/latest/?badge=latest) [![Downloads](https://img.shields.io/pypi/dd/aio-openapi.svg)](https://pypi.org/project/aio-openapi/) From ae87227e8066f7808eab1439f7cca8c60f27851f Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 9 Jan 2021 16:19:47 +0000 Subject: [PATCH 2/6] point to the correct coverage report --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index a39f14d..0229438 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ [![PyPI version](https://badge.fury.io/py/aio-openapi.svg)](https://badge.fury.io/py/aio-openapi) [![Python versions](https://img.shields.io/pypi/pyversions/aio-openapi.svg)](https://pypi.org/project/aio-openapi) [![Build](https://github.com/quantmind/aio-openapi/workflows/build/badge.svg)](https://github.com/quantmind/aio-openapi/actions?query=workflow%3Abuild) -[![Coverage Status](https://coveralls.io/repos/github/quantmind/aio-openapi/badge.svg?branch=HEAD)](https://coveralls.io/github/quantmind/aio-openapi?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/quantmind/aio-openapi/badge.svg?branch=master)](https://coveralls.io/github/quantmind/aio-openapi?branch=master) [![Documentation Status](https://readthedocs.org/projects/aio-openapi/badge/?version=latest)](https://aio-openapi.readthedocs.io/en/latest/?badge=latest) [![Downloads](https://img.shields.io/pypi/dd/aio-openapi.svg)](https://pypi.org/project/aio-openapi/) From 80b0fa0fc084fd5b6b8109f1237b4c030cb187ef Mon Sep 17 00:00:00 2001 From: Luca Sbardella Date: Mon, 11 Jan 2021 21:06:46 +0000 Subject: [PATCH 3/6] Update Makefile --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ecddcc8..b04bfaf 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ -# Minimal makefile for Sphinx documentation -# +# Makefile for development & CI .PHONY: help clean docs From 3a6d62989d2e07bf02141c641a418fe985bc540c Mon Sep 17 00:00:00 2001 From: Luca Sbardella Date: Tue, 12 Jan 2021 08:37:48 +0000 Subject: [PATCH 4/6] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 3f77214..9e23155 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2020 Quantmind +Copyright (c) 2021 Quantmind Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: From 549f45cf54412e75d6dd86fd42b5b8c9170817dc Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 14 Feb 2021 16:54:01 +0000 Subject: [PATCH 5/6] Use sentry sdk and drop support for python 3.6 --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- Makefile | 3 -- dev/requirements-test.txt | 2 +- openapi/__init__.py | 2 +- openapi/_py36.py | 52 --------------------- openapi/db/container.py | 2 +- openapi/middleware.py | 13 ++++++ openapi/sentry.py | 86 ++++++++--------------------------- openapi/testing.py | 2 +- openapi/utils.py | 22 +-------- setup.py | 1 - tests/conftest.py | 8 ++-- tests/core/test_sentry.py | 17 ------- tests/example/main.py | 7 +-- 15 files changed, 47 insertions(+), 174 deletions(-) delete mode 100644 openapi/_py36.py delete mode 100644 tests/core/test_sentry.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3796015..442a4a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50f1736..94b8d3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.QMBOT_GITHUB_TOKEN }} strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 - name: Set up Python diff --git a/Makefile b/Makefile index b04bfaf..77e1b9e 100644 --- a/Makefile +++ b/Makefile @@ -63,9 +63,6 @@ test-version: ## validate version with pypi @agilekit git validate -bundle3.6: ## build python 3.6 bundle - @python setup.py bdist_wheel --python-tag py36 - bundle3.7: ## build python 3.7 bundle @python setup.py bdist_wheel --python-tag py37 diff --git a/dev/requirements-test.txt b/dev/requirements-test.txt index 6c00395..9a8a634 100644 --- a/dev/requirements-test.txt +++ b/dev/requirements-test.txt @@ -28,6 +28,6 @@ twine agile-toolkit # additional features -raven-aiohttp +sentry-sdk python-dotenv openapi-spec-validator diff --git a/openapi/__init__.py b/openapi/__init__.py index bbd232d..45d5392 100644 --- a/openapi/__init__.py +++ b/openapi/__init__.py @@ -1,3 +1,3 @@ """Minimal OpenAPI asynchronous server application""" -__version__ = "2.1.2" +__version__ = "2.2.0" diff --git a/openapi/_py36.py b/openapi/_py36.py deleted file mode 100644 index 6fb3b31..0000000 --- a/openapi/_py36.py +++ /dev/null @@ -1,52 +0,0 @@ -from functools import wraps - - -def asynccontextmanager(func): - @wraps(func) - def helper(*args, **kwds): - return _AsyncGeneratorContextManager(func, args, kwds) - - return helper - - -class _AsyncGeneratorContextManager: - def __init__(self, func, args, kwds): - self.gen = func(*args, **kwds) - self.func, self.args, self.kwds = func, args, kwds - doc = getattr(func, "__doc__", None) - if doc is None: - doc = type(self).__doc__ - self.__doc__ = doc - - async def __aenter__(self): - try: - return await self.gen.__anext__() - except StopAsyncIteration: - raise RuntimeError("generator didn't yield") from None - - async def __aexit__(self, typ, value, traceback): - if typ is None: - try: - await self.gen.__anext__() - except StopAsyncIteration: - return - else: - raise RuntimeError("generator didn't stop") - else: - if value is None: - value = typ() - try: - await self.gen.athrow(typ, value, traceback) - raise RuntimeError("generator didn't stop after throw()") - except StopAsyncIteration as exc: - return exc is not value - except RuntimeError as exc: - if exc is value: - return False - if isinstance(value, (StopIteration, StopAsyncIteration)): - if exc.__cause__ is value: - return False - raise - except BaseException as exc: - if exc is not value: - raise diff --git a/openapi/db/container.py b/openapi/db/container.py index e2b6e89..f98236d 100644 --- a/openapi/db/container.py +++ b/openapi/db/container.py @@ -1,5 +1,6 @@ import asyncio import os +from contextlib import asynccontextmanager from typing import Any, Optional import asyncpg @@ -8,7 +9,6 @@ from asyncpg.pool import Pool from ..exc import ImproperlyConfigured -from ..utils import asynccontextmanager DBPOOL_MIN_SIZE = int(os.environ.get("DBPOOL_MIN_SIZE") or "10") DBPOOL_MAX_SIZE = int(os.environ.get("DBPOOL_MAX_SIZE") or "10") diff --git a/openapi/middleware.py b/openapi/middleware.py index d78d787..aa8d946 100644 --- a/openapi/middleware.py +++ b/openapi/middleware.py @@ -2,9 +2,22 @@ from aiohttp import web +from .exc import ImproperlyConfigured + +try: + from . import sentry +except ImportError: # pragma: no cover + sentry = None + ERROR_500 = os.environ.get("ERROR_500_MESSSAGE", "Internal Server Error") +def sentry_middleware(app, dsn, env="dev"): + if not sentry: # pragma: no cover + raise ImproperlyConfigured("Sentry middleware requires sentry-sdk") + sentry.setup(app, dsn, env) + + def json_error(status_codes=None): status_codes = set(status_codes or (404, 405, 500)) content_type = "application/json" diff --git a/openapi/sentry.py b/openapi/sentry.py index 91355c4..93316b6 100644 --- a/openapi/sentry.py +++ b/openapi/sentry.py @@ -1,66 +1,20 @@ -from aiohttp import web - -from .exc import ImproperlyConfigured - -try: - from raven import Client - from raven.conf.remote import RemoteConfig - from raven_aiohttp import AioHttpTransport -except ImportError: # pragma: no cover - AioHttpTransport = None - - -def middleware(app, dsn, env="dev"): - if not AioHttpTransport: # pragma: no cover - raise ImproperlyConfigured("Sentry middleware requires raven_aiohttp") - app["sentry"] = Sentry(dsn, env) - app.on_shutdown.append(close) - - @web.middleware - async def middleware_handler(request, handler): - try: - return await handler(request) - except Exception: - content = await request.content.read() - data = { - "request": { - "url": str(request.url).split("?")[0], - "method": request.method.lower(), - "data": content, - "query_string": request.url.query_string, - "cookies": dict(request.cookies), - "headers": dict(request.headers), - }, - "user": {"id": request.get("user_id")}, - } - app["sentry"].captureException(data=data) - raise - - return middleware_handler - - -async def close(app): - await app["sentry"].close() - - -class Sentry: - def __init__(self, dsn, env): - client = Client( - transport=AioHttpTransport, ignore_exceptions=[web.HTTPException] - ) - client.remote = RemoteConfig(transport=AioHttpTransport) - client._transport_cache = {None: client.remote} - client.set_dsn(dsn, AioHttpTransport) - self.env = env - self.client = client - - def captureException(self, data=None): - if data is None: - data = {} - data["environment"] = self.env - self.client.captureException(data=data) - - async def close(self): - transport = self.client.remote.get_transport() - if transport: - await transport.close() +import logging + +import sentry_sdk +from sentry_sdk.integrations.aiohttp import AioHttpIntegration +from sentry_sdk.integrations.logging import LoggingIntegration + + +def setup(app, dsn, env="dev", level=logging.ERROR, event_level=logging.ERROR): + + sentry_sdk.init( + dsn=dsn, + environment=env, + integrations=[ + LoggingIntegration( + level=level, # Capture level and above as breadcrumbs + event_level=event_level, # Send event_level and above as events + ), + AioHttpIntegration(), + ], + ) diff --git a/openapi/testing.py b/openapi/testing.py index 6ee61f4..de2c5de 100644 --- a/openapi/testing.py +++ b/openapi/testing.py @@ -1,6 +1,7 @@ """Testing utilities """ import asyncio +from contextlib import asynccontextmanager from typing import Any from aiohttp.client import ClientResponse @@ -9,7 +10,6 @@ from .db.dbmodel import CrudDB from .json import dumps, loads -from .utils import asynccontextmanager async def json_body(response: ClientResponse, status: int = 200) -> Any: diff --git a/openapi/utils.py b/openapi/utils.py index 11bf42b..ef15fb6 100644 --- a/openapi/utils.py +++ b/openapi/utils.py @@ -18,27 +18,9 @@ from .exc import InvalidTypeException -if sys.version_info >= (3, 7): - from contextlib import asynccontextmanager # noqa - def get_origin(value: Any) -> Any: - return getattr(value, "__origin__", None) - - -else: # pragma: no cover - from ._py36 import asynccontextmanager # noqa - - py36_origins = {List: list, Dict: dict} - - def get_origin(value: Any) -> Any: - try: - if value in py36_origins: - origin = value - else: - origin = getattr(value, "__origin__", None) - except TypeError: - origin = getattr(value, "__origin__", None) - return py36_origins.get(origin, origin) +def get_origin(value: Any) -> Any: + return getattr(value, "__origin__", None) if sys.version_info >= (3, 8): diff --git a/setup.py b/setup.py index 319d002..637f179 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,6 @@ def requirements(name): "Programming Language :: JavaScript", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", diff --git a/tests/conftest.py b/tests/conftest.py index 59df125..5c1ea3f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,11 @@ import asyncio import os import shutil +from unittest import mock import pytest from aiohttp import test_utils from aiohttp.web import Application -from asynctest import CoroutineMock from sqlalchemy_utils import create_database, database_exists from openapi.db.dbmodel import CrudDB @@ -23,9 +23,9 @@ def clean_migrations(): @pytest.fixture(autouse=True) def sentry_mock(mocker): - mock = CoroutineMock() - mocker.patch("raven_aiohttp.AioHttpTransport._do_send", mock) - return mock + mm = mock.MagicMock() + mocker.patch("sentry_sdk.init", mm) + return mm @pytest.fixture(scope="session", autouse=True) diff --git a/tests/core/test_sentry.py b/tests/core/test_sentry.py deleted file mode 100644 index 4866a90..0000000 --- a/tests/core/test_sentry.py +++ /dev/null @@ -1,17 +0,0 @@ -import json -import zlib - -from openapi.testing import jsonBody - - -async def test_sentry(cli, sentry_mock, mocker): - resp = await cli.get("/error") - await jsonBody(resp, 500) - - calls = [json.loads(zlib.decompress(a[0][1])) for a in sentry_mock.call_args_list] - assert len(calls) == 1 - assert calls[0]["environment"] == "test" - middleware_call = calls[0] - request_info = middleware_call["request"] - expected_keys = {"cookies", "data", "headers", "method", "query_string", "url"} - assert set(request_info).issuperset(expected_keys) diff --git a/tests/example/main.py b/tests/example/main.py index f7b7bd8..f8d4f73 100644 --- a/tests/example/main.py +++ b/tests/example/main.py @@ -2,9 +2,8 @@ from aiohttp import web -from openapi import sentry from openapi.db.commands import db as db_command -from openapi.middleware import json_error +from openapi.middleware import json_error, sentry_middleware from openapi.rest import rest from openapi.spec import Redoc @@ -39,9 +38,7 @@ def create_app(): def setup_app(app: web.Application) -> None: db.setup(app) app.middlewares.append(json_error()) - app.middlewares.append( - sentry.middleware(app, f"https://{uuid.uuid4().hex}@sentry.io/1234567", "test") - ) + sentry_middleware(app, f"https://{uuid.uuid4().hex}@sentry.io/1234567", "test") app.router.add_routes(base_routes) app.router.add_routes(routes) # From 3e50270cc6a68fadc04814345575384fed641637 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 14 Feb 2021 16:58:38 +0000 Subject: [PATCH 6/6] 3.7 minimum version --- .github/workflows/build.yml | 2 +- setup.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 442a4a9..a647609 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,5 +34,5 @@ jobs: - name: run tests run: make test - name: upload coverage - if: matrix.python-version == '3.8' + if: matrix.python-version == '3.9' run: coveralls diff --git a/setup.py b/setup.py index 637f179..4ac4539 100644 --- a/setup.py +++ b/setup.py @@ -33,9 +33,6 @@ def requirements(name): install_requires = requirements("dev/requirements.txt")[0] tests_require = requirements("dev/requirements-test.txt")[0] -if sys.version_info < (3, 7): - install_requires.append("dataclasses") - if sys.version_info < (3, 8): install_requires.append("cached-property") @@ -52,7 +49,7 @@ def requirements(name): author_email="luca@quantmind.com", maintainer_email="luca@quantmind.com", url="https://github.com/quantmind/aio-openapi", - python_requires=">=3.6", + python_requires=">=3.7", install_requires=install_requires, tests_require=tests_require, include_package_data=True,