diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59a77d18..dbf2cc00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,47 +14,49 @@ jobs: matrix: python-version: [3.6, 3.7, 3.8, 3.9, '3.10', 3.11] framework: + - NONE - FLASK_VERSION=1.1.4 - - FLASK_VERSION=2.2.3 - - DJANGO_VERSION=1.11.29 - - DJANGO_VERSION=2.2.28 - - DJANGO_VERSION=3.2.18 - - DJANGO_VERSION=4.0.10 - - DJANGO_VERSION=4.1.7 + - FLASK_VERSION=2.3.3 + - FLASK_VERSION=3.0.3 + - DJANGO_VERSION=3.2.25 + - DJANGO_VERSION=4.2.15 + - DJANGO_VERSION=5.0.8 - TWISTED_VERSION=20.3.0 - TWISTED_VERSION=21.7.0 - TWISTED_VERSION=22.10.0 - PYRAMID_VERSION=1.10.8 - - STARLETTE_VERSION=0.12.13 httpx==0.18.1 python-multipart==0.0.5 - - STARLETTE_VERSION=0.14.2 httpx==0.18.1 python-multipart==0.0.5 - - FASTAPI_VERSION=0.40.0 httpx==0.18.1 python-multipart==0.0.5 - - FASTAPI_VERSION=0.50.0 httpx==0.18.1 python-multipart==0.0.5 - - FASTAPI_VERSION=0.63.0 httpx==0.18.1 python-multipart==0.0.5 + - PYRAMID_VERSION=2.0.2 + - STARLETTE_VERSION=0.30.0 httpx==0.24.1 python-multipart==0.0.9 + - STARLETTE_VERSION=0.38.2 httpx==0.27.0 python-multipart==0.0.9 + - FASTAPI_VERSION=0.101.1 httpx==0.24.1 python-multipart==0.0.9 + - FASTAPI_VERSION=0.112.1 httpx==0.27.0 python-multipart==0.0.9 exclude: # Test frameworks on the python versions they support, according to pypi registry # Flask - - framework: FLASK_VERSION=2.2.3 + - framework: FLASK_VERSION=2.3.3 python-version: 3.6 + - framework: FLASK_VERSION=2.3.3 + python-version: 3.7 + - framework: FLASK_VERSION=3.0.3 + python-version: 3.6 + - framework: FLASK_VERSION=3.0.3 + python-version: 3.7 # Django - - framework: DJANGO_VERSION=1.11.29 - python-version: 3.8 - - framework: DJANGO_VERSION=1.11.29 - python-version: 3.9 - - framework: DJANGO_VERSION=1.11.29 - python-version: '3.10' - - framework: DJANGO_VERSION=1.11.29 + - framework: DJANGO_VERSION=3.2.25 python-version: 3.11 - - framework: DJANGO_VERSION=4.0.10 + - framework: DJANGO_VERSION=4.2.15 python-version: 3.6 - - framework: DJANGO_VERSION=4.0.10 + - framework: DJANGO_VERSION=4.2.15 python-version: 3.7 - - framework: DJANGO_VERSION=4.1.7 - python-version: 3.5 - - framework: DJANGO_VERSION=4.1.7 + - framework: DJANGO_VERSION=5.0.8 python-version: 3.6 - - framework: DJANGO_VERSION=4.1.7 + - framework: DJANGO_VERSION=5.0.8 python-version: 3.7 + - framework: DJANGO_VERSION=5.0.8 + python-version: 3.8 + - framework: DJANGO_VERSION=5.0.8 + python-version: 3.9 # Twisted - framework: TWISTED_VERSION=20.3.0 @@ -62,6 +64,26 @@ jobs: - framework: TWISTED_VERSION=22.10.0 python-version: 3.6 + # Starlette + - framework: STARLETTE_VERSION=0.30.0 httpx==0.24.1 python-multipart==0.0.9 + python-version: 3.6 + - framework: STARLETTE_VERSION=0.30.0 httpx==0.24.1 python-multipart==0.0.9 + python-version: 3.7 + - framework: STARLETTE_VERSION=0.38.2 httpx==0.27.0 python-multipart==0.0.9 + python-version: 3.6 + - framework: STARLETTE_VERSION=0.38.2 httpx==0.27.0 python-multipart==0.0.9 + python-version: 3.7 + + # FastAPI + - framework: FASTAPI_VERSION=0.101.1 httpx==0.24.1 python-multipart==0.0.9 + python-version: 3.6 + - framework: FASTAPI_VERSION=0.101.1 httpx==0.24.1 python-multipart==0.0.9 + python-version: 3.7 + - framework: FASTAPI_VERSION=0.112.1 httpx==0.27.0 python-multipart==0.0.9 + python-version: 3.6 + - framework: FASTAPI_VERSION=0.112.1 httpx==0.27.0 python-multipart==0.0.9 + python-version: 3.7 + steps: - uses: actions/checkout@v2 with: @@ -75,7 +97,7 @@ jobs: - name: Install Python 3.6 dependencies if: ${{ contains(matrix.python-version, '3.6') }} # typing-extensions dropped support for Python 3.6 in version 4.2 - run: pip install "typing-extensions<4.2" requests==2.27.0 blinker==1.5 immutables==0.19 + run: pip install "typing-extensions<4.2" requests==2.27.0 blinker==1.5 immutables==0.19 webob blinker httpx aiocontextvars - name: Install Python 3.7 dependencies if: ${{ contains(matrix.python-version, '3.7') }} @@ -83,6 +105,7 @@ jobs: run: pip install immutables==0.19 - name: Set the framework + if: ${{ matrix.framework != 'NONE' }} run: echo ${{ matrix.framework }} >> $GITHUB_ENV - name: Install Flask @@ -109,5 +132,13 @@ jobs: if: ${{ contains(matrix.framework, 'FASTAPI_VERSION') }} run: pip install fastapi==$FASTAPI_VERSION - - name: Run tests - run: python setup.py test + - name: Install Tox + run: pip install tox + + - name: Run tests (skip on Python 3.6) + if: ${{ !contains(matrix.python-version, '3.6') }} + run: tox + + - name: Run tests (Python 3.6) + if: ${{ contains(matrix.python-version, '3.6') }} + run: python -m unittest rollbar.test.discover diff --git a/.gitignore b/.gitignore index f82be943..4cbc538a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ Pipfile Pipfile.lock .pytest_cache/ .python-version +.tox/ diff --git a/README.md b/README.md index 59006709..5a476da6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Python notifier for reporting exceptions, errors, and log messages to [Rollbar]( - **Advanced search:** Filter items by many different properties. Learn more about search. - **Customizable notifications:** Rollbar supports several messaging and incident management tools where your team can get notified about errors and important events by real-time alerts. Learn more about Rollbar notifications. -## Versions Supported +## Python Versions Supported | PyRollbar Version | Python Version Compatibility | Support Level | |-------------------|-----------------------------------------------|---------------------| @@ -33,6 +33,26 @@ Python notifier for reporting exceptions, errors, and log messages to [Rollbar]( **Security Fixes Only** - We will only provide critical security fixes for the library. +## Frameworks Supported + +Generally, PyRollbar can be used with any Python framework. However, we have official support for the following frameworks: + +| Framework | Support Duration | Tested Versions | +|-----------|----------------------------|-----------------| +| Celery | Release +1 year | None | +| Django | Release or LTS end +1 year | 3.2, 4.2, 5.0 | +| FastAPI | Release +1 year | 0.101, 0.112 | +| Flask | Release +1 year | 1.1, 2.3, 3.0 | +| Pyramid | Release +1 year | 1.10, 2.0 | + +Official support means that we ship and maintain integrations for these frameworks. It also means that we test against these frameworks as part of our CI pipeline. + +Generally, we will support the last year of releases for a framework. If a framework has a defined support period (including LTS releases), we will support the release for the duration of that period plus one year. + +### Community Supported + +There are also a number of community-supported integrations available. For more information, see the [Python SDK docs](https://docs.rollbar.com/docs/python-community-supported-sdks). + ## Setup Instructions 1. [Sign up for a Rollbar account](https://rollbar.com/signup) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..9595f351 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,54 @@ +[project] +name = "rollbar" +dynamic = ["version"] +description = "Easy and powerful exception tracking with Rollbar. Send messages and exceptions with arbitrary context, get back aggregates, and debug production issues quickly." +readme = "README.md" +license = {file = "LICENSE"} +maintainers = [{name = "Rollbar, Inc.", email = "support@rollbar.com"}] +classifiers = [ + "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", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: AsyncIO", + "Framework :: Bottle", + "Framework :: Django", + "Framework :: Flask", + "Framework :: Pylons", + "Framework :: Pyramid", + "Framework :: Twisted", + "Intended Audience :: Developers", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development", + "Topic :: Software Development :: Bug Tracking", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Quality Assurance", + "Topic :: System :: Logging", + "Topic :: System :: Monitoring", +] +requires-python = ">=3.6" +dependencies = [ + "requests>=0.12.1", +] + +[project.urls] +Homepage = "https://rollbar.com/" +Documentation = "https://docs.rollbar.com/docs/python" +Changes = "https://github.com/rollbar/pyrollbar/blob/master/CHANGELOG.md" +Source = "https://github.com/rollbar/pyrollbar/" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic] +version = {attr = "rollbar.__version__"} diff --git a/rollbar/__init__.py b/rollbar/__init__.py index 905fd9a5..d2c82edd 100644 --- a/rollbar/__init__.py +++ b/rollbar/__init__.py @@ -1372,7 +1372,7 @@ def _build_starlette_request_data(request): 'params': dict(request.path_params), } - if hasattr(request, '_form'): + if hasattr(request, '_form') and request._form is not None: request_data['POST'] = { k: v.filename if isinstance(v, UploadFile) else v for k, v in request._form.items() @@ -1772,4 +1772,8 @@ def _wsgi_extract_user_ip(environ): def _starlette_extract_user_ip(request): + if not hasattr(request, 'client'): + return _extract_user_ip_from_headers(request) + if not hasattr(request.client, 'host'): + return _extract_user_ip_from_headers(request) return request.client.host or _extract_user_ip_from_headers(request) diff --git a/rollbar/lib/transforms/__init__.py b/rollbar/lib/transforms/__init__.py index 3f1b8802..4923b9c3 100644 --- a/rollbar/lib/transforms/__init__.py +++ b/rollbar/lib/transforms/__init__.py @@ -34,6 +34,8 @@ def transform(obj, transforms, key=None, batch_transforms=False): transforms = [BatchedTransform(transforms)] for transform in transforms: + if not isinstance(transform, Transform): + continue obj = _transform(obj, transform, key=key) return obj diff --git a/rollbar/test/fastapi_tests/test_logger.py b/rollbar/test/fastapi_tests/test_logger.py index 49a4a8a6..99a8d9a2 100644 --- a/rollbar/test/fastapi_tests/test_logger.py +++ b/rollbar/test/fastapi_tests/test_logger.py @@ -38,6 +38,7 @@ def test_should_add_framework_version_to_payload(self, mock_send_payload, *mocks app = FastAPI() app.add_middleware(LoggerMiddleware) + app.build_middleware_stack() rollbar.report_exc_info() @@ -70,10 +71,10 @@ def test_should_store_current_request(self, store_current_request): 'client': ['testclient', 50000], 'headers': [ (b'host', b'testserver'), - (b'user-agent', b'testclient'), - (b'accept-encoding', b'gzip, deflate'), (b'accept', b'*/*'), + (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive'), + (b'user-agent', b'testclient'), ], 'http_version': '1.1', 'method': 'GET', diff --git a/rollbar/test/fastapi_tests/test_middleware.py b/rollbar/test/fastapi_tests/test_middleware.py index c49336e4..023754a0 100644 --- a/rollbar/test/fastapi_tests/test_middleware.py +++ b/rollbar/test/fastapi_tests/test_middleware.py @@ -16,6 +16,7 @@ import rollbar from rollbar.lib._async import AsyncMock from rollbar.test import BaseTest +from rollbar.test.utils import get_public_attrs ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) @@ -152,6 +153,7 @@ def test_should_add_framework_version_to_payload(self, mock_send_payload, *mocks app = FastAPI() app.add_middleware(ReporterMiddleware) + app.build_middleware_stack() rollbar.report_exc_info() @@ -272,10 +274,10 @@ def test_should_store_current_request(self, store_current_request): 'client': ['testclient', 50000], 'headers': [ (b'host', b'testserver'), - (b'user-agent', b'testclient'), - (b'accept-encoding', b'gzip, deflate'), (b'accept', b'*/*'), + (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive'), + (b'user-agent', b'testclient'), ], 'http_version': '1.1', 'method': 'GET', @@ -324,7 +326,7 @@ def test_should_return_current_request(self): async def read_root(original_request: Request): request = get_current_request() - self.assertEqual(request, original_request) + self.assertEqual(get_public_attrs(request), get_public_attrs(original_request)) client = TestClient(app) client.get('/') diff --git a/rollbar/test/starlette_tests/test_logger.py b/rollbar/test/starlette_tests/test_logger.py index 3ed51e68..5981e5cb 100644 --- a/rollbar/test/starlette_tests/test_logger.py +++ b/rollbar/test/starlette_tests/test_logger.py @@ -38,6 +38,7 @@ def test_should_add_framework_version_to_payload(self, mock_send_payload, *mocks app = Starlette() app.add_middleware(LoggerMiddleware) + app.build_middleware_stack() rollbar.report_exc_info() @@ -67,10 +68,10 @@ def test_should_store_current_request(self, store_current_request): 'client': ['testclient', 50000], 'headers': [ (b'host', b'testserver'), - (b'user-agent', b'testclient'), - (b'accept-encoding', b'gzip, deflate'), (b'accept', b'*/*'), + (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive'), + (b'user-agent', b'testclient'), ], 'http_version': '1.1', 'method': 'GET', diff --git a/rollbar/test/starlette_tests/test_middleware.py b/rollbar/test/starlette_tests/test_middleware.py index 7c9f6554..e7415af0 100644 --- a/rollbar/test/starlette_tests/test_middleware.py +++ b/rollbar/test/starlette_tests/test_middleware.py @@ -16,6 +16,7 @@ import rollbar from rollbar.lib._async import AsyncMock from rollbar.test import BaseTest +from rollbar.test.utils import get_public_attrs ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) @@ -138,6 +139,7 @@ def test_should_add_framework_version_to_payload(self, mock_send_payload, *mocks app = Starlette() app.add_middleware(ReporterMiddleware) + app.build_middleware_stack() rollbar.report_exc_info() @@ -243,10 +245,10 @@ def test_should_store_current_request(self, store_current_request): 'client': ['testclient', 50000], 'headers': [ (b'host', b'testserver'), - (b'user-agent', b'testclient'), - (b'accept-encoding', b'gzip, deflate'), (b'accept', b'*/*'), + (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive'), + (b'user-agent', b'testclient'), ], 'http_version': '1.1', 'method': 'GET', @@ -290,7 +292,7 @@ def test_should_return_current_request(self): async def root(original_request): request = get_current_request() - self.assertEqual(request, original_request) + self.assertEqual(get_public_attrs(request), get_public_attrs(original_request)) return PlainTextResponse('OK') diff --git a/rollbar/test/starlette_tests/test_requests.py b/rollbar/test/starlette_tests/test_requests.py index 75bacb1d..09b0c292 100644 --- a/rollbar/test/starlette_tests/test_requests.py +++ b/rollbar/test/starlette_tests/test_requests.py @@ -10,6 +10,7 @@ import unittest from rollbar.test import BaseTest +from rollbar.test.utils import get_public_attrs ALLOWED_PYTHON_VERSION = sys.version_info >= (3, 6) @@ -49,7 +50,7 @@ def test_should_accept_request_param(self): stored_request = store_current_request(request) - self.assertEqual(request, stored_request) + self.assertEqual(get_public_attrs(request), get_public_attrs(stored_request)) def test_should_accept_scope_param_if_http_type(self): from starlette.requests import Request @@ -81,7 +82,7 @@ def test_should_accept_scope_param_if_http_type(self): request = store_current_request(scope, receive) - self.assertEqual(request, expected_request) + self.assertEqual(get_public_attrs(request), get_public_attrs(expected_request)) def test_should_not_accept_scope_param_if_not_http_type(self): from rollbar.contrib.starlette.requests import store_current_request diff --git a/rollbar/test/test_rollbar.py b/rollbar/test/test_rollbar.py index 3107e9e4..35b69766 100644 --- a/rollbar/test/test_rollbar.py +++ b/rollbar/test/test_rollbar.py @@ -20,6 +20,7 @@ from rollbar.lib import string_types from rollbar.test import BaseTest +from rollbar.test.utils import get_public_attrs try: eval(""" @@ -173,6 +174,7 @@ def test_starlette_request_data_with_consumed_body(self): body = b'body body body' scope = { 'type': 'http', + 'client': ('127.0.0.1', 1453), 'headers': [ (b'content-type', b'text/html'), (b'content-length', str(len(body)).encode('latin-1')), @@ -410,7 +412,7 @@ def test_get_request_starlette_middleware(self): def root(starlette_request): current_request = rollbar.get_request() - self.assertEqual(current_request, starlette_request) + self.assertEqual(get_public_attrs(current_request), get_public_attrs(starlette_request)) return PlainTextResponse("bye bye") @@ -437,7 +439,7 @@ def test_get_request_starlette_logger(self): def root(starlette_request): current_request = rollbar.get_request() - self.assertEqual(current_request, starlette_request) + self.assertEqual(get_public_attrs(current_request), get_public_attrs(starlette_request)) return PlainTextResponse("bye bye") @@ -465,7 +467,7 @@ def test_get_request_fastapi_middleware(self): def root(param, fastapi_request: Request): current_request = rollbar.get_request() - self.assertEqual(current_request, fastapi_request) + self.assertEqual(get_public_attrs(current_request), get_public_attrs(fastapi_request)) root = fastapi_add_route_with_request_param( app, root, '/{param}', 'fastapi_request' @@ -492,7 +494,7 @@ def test_get_request_fastapi_logger(self): def root(fastapi_request: Request): current_request = rollbar.get_request() - self.assertEqual(current_request, fastapi_request) + self.assertEqual(get_public_attrs(current_request), get_public_attrs(fastapi_request)) root = fastapi_add_route_with_request_param( app, root, '/{param}', 'fastapi_request' @@ -523,7 +525,7 @@ def test_get_request_fastapi_router(self): def root(fastapi_request: Request): current_request = rollbar.get_request() - self.assertEqual(current_request, fastapi_request) + self.assertEqual(get_public_attrs(current_request), get_public_attrs(fastapi_request)) root = fastapi_add_route_with_request_param( app, root, '/{param}', 'fastapi_request' diff --git a/rollbar/test/utils.py b/rollbar/test/utils.py new file mode 100644 index 00000000..88294592 --- /dev/null +++ b/rollbar/test/utils.py @@ -0,0 +1,4 @@ +from collections.abc import Mapping + +def get_public_attrs(obj: Mapping) -> dict: + return {k: obj[k] for k in obj if not k.startswith('_')} diff --git a/setup.py b/setup.py deleted file mode 100644 index 87f4fc1a..00000000 --- a/setup.py +++ /dev/null @@ -1,79 +0,0 @@ -import re -import os.path -from setuptools import setup, find_packages - -HERE = os.path.abspath(os.path.dirname(__file__)) - -README_PATH = os.path.join(HERE, 'README.md') -try: - with open(README_PATH) as fd: - README = fd.read() -except IOError: - README = '' - -INIT_PATH = os.path.join(HERE, 'rollbar/__init__.py') -with open(INIT_PATH) as fd: - INIT_DATA = fd.read() - VERSION = re.search(r"^__version__ = ['\"]([^'\"]+)['\"]", INIT_DATA, re.MULTILINE).group(1) - -tests_require = [ - 'webob', - 'blinker', - 'httpx', - 'aiocontextvars; python_version == "3.6"' -] - -setup( - name='rollbar', - packages=find_packages(), - version=VERSION, - entry_points={ - 'paste.filter_app_factory': [ - 'pyramid=rollbar.contrib.pyramid:create_rollbar_middleware' - ], - 'console_scripts': ['rollbar=rollbar.cli:main'] - }, - description='Easy and powerful exception tracking with Rollbar. Send ' - 'messages and exceptions with arbitrary context, get back ' - 'aggregates, and debug production issues quickly.', - long_description=README, - long_description_content_type="text/markdown", - author='Rollbar, Inc.', - author_email='support@rollbar.com', - test_suite='rollbar.test.discover', - url='http://github.com/rollbar/pyrollbar', - classifiers=[ - "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", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3 :: Only", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Framework :: AsyncIO", - "Framework :: Bottle", - "Framework :: Django", - "Framework :: Flask", - "Framework :: Pylons", - "Framework :: Pyramid", - "Framework :: Twisted", - "Intended Audience :: Developers", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Software Development", - "Topic :: Software Development :: Bug Tracking", - "Topic :: Software Development :: Testing", - "Topic :: Software Development :: Quality Assurance", - "Topic :: System :: Logging", - "Topic :: System :: Monitoring", - ], - install_requires=[ - 'requests>=0.12.1', - ], - tests_require=tests_require, -) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..a05ac4b2 --- /dev/null +++ b/tox.ini @@ -0,0 +1,9 @@ +[tox] +requires = tox>=4 + +[testenv] +deps = webob + blinker + httpx +description = run unit tests +commands = python -m unittest rollbar.test.discover