From 0f84291085de21166632617e12e74af376d4bd74 Mon Sep 17 00:00:00 2001 From: Jacobi Petrucciani Date: Mon, 24 Sep 2018 11:34:49 -0400 Subject: [PATCH] first commit --- .gitignore | 127 +++++++++++++++++++++++++++++++ .prospector.yaml | 76 ++++++++++++++++++ .travis.yml | 18 +++++ LICENSE | 21 +++++ Makefile | 15 ++++ README.rst | 41 ++++++++++ dev-requirements.txt | 4 + pybugsnag/__init__.py | 4 + pybugsnag/globals.py | 14 ++++ pybugsnag/models/__init__.py | 107 ++++++++++++++++++++++++++ pybugsnag/models/client.py | 76 ++++++++++++++++++ pybugsnag/models/error.py | 22 ++++++ pybugsnag/test/__init__.py | 3 + pybugsnag/test/conftest.py | 17 +++++ pybugsnag/test/helpers.py | 28 +++++++ pybugsnag/test/test_pybugsnag.py | 7 ++ pybugsnag/utils/__init__.py | 3 + pybugsnag/utils/text.py | 38 +++++++++ requirements.txt | 1 + setup.cfg | 12 +++ setup.py | 32 ++++++++ 21 files changed, 666 insertions(+) create mode 100644 .gitignore create mode 100644 .prospector.yaml create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.rst create mode 100644 dev-requirements.txt create mode 100644 pybugsnag/__init__.py create mode 100644 pybugsnag/globals.py create mode 100644 pybugsnag/models/__init__.py create mode 100644 pybugsnag/models/client.py create mode 100644 pybugsnag/models/error.py create mode 100644 pybugsnag/test/__init__.py create mode 100644 pybugsnag/test/conftest.py create mode 100644 pybugsnag/test/helpers.py create mode 100644 pybugsnag/test/test_pybugsnag.py create mode 100644 pybugsnag/utils/__init__.py create mode 100644 pybugsnag/utils/text.py create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d244d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,127 @@ +tmp.py +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +### Python Patch ### +.venv/ + +### Python.VirtualEnv Stack ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json diff --git a/.prospector.yaml b/.prospector.yaml new file mode 100644 index 0000000..185d398 --- /dev/null +++ b/.prospector.yaml @@ -0,0 +1,76 @@ +strictness: veryhigh +doc-warnings: true +member-warnings: false +test-warnings: false + +ignore-patterns: + - (^|/)\..+ + - .*\.html + +pylint: + disable: + - bad-continuation + - broad-except + - import-error + - import-self + - logging-format-interpolation + - missing-docstring + - no-self-use + - unused-argument + - wrong-import-order + + options: + max-args: 10 + max-locals: 100 + max-returns: 6 + max-branches: 50 + max-statements: 180 + max-parents: 10 + max-attributes: 10 + min-public-methods: 0 + max-public-methods: 20 + max-module-lines: 2000 + max-line-length: 100 + +mccabe: + options: + max-complexity: 30 + +pep8: + disable: + - N802 + - N807 + - W503 + options: + max-line-length: 100 + single-line-if-stmt: n + +vulture: + run: false + +pyroma: + run: false + disable: + - PYR19 + - PYR16 + +pep257: + disable: + - D000 + - D100 + - D101 + - D102 + - D103 + - D104 + - D105 + - D107 + - D200 + - D203 + - D205 + - D212 + - D204 + - D300 + - D400 + - D401 + - D404 + - D403 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0807ced --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: python +dist: xenial +python: + - '3.6' +install: + - pip install -r requirements.txt + - pip install -r dev-requirements.txt +script: make +after_script: cd ~ +after_success: coveralls --verbose +deploy: + provider: pypi + user: jacobi + on: + branch: master + tags: true + password: + secure: E8P4ExPSoMqU2gkvtkfrdckiN7K5USh6YOcZGjcPOIMcJFF/5bYLinilUVEb8KxqYiDpgl/g0LhZKJYjy7BzBCk3zYQhRHpKTb+jW+EdPS7EXjbSgIDUBA6KXb0T3Pn6Kq63M/xA1j+cJ1I5l5HElvMaYuvra3QIAfexlfxKlQLuPq3sW0ok8216f5dLe09FBwcnGubdXpThPQoqDv7RbBH9W++bUc3a/SXi5UaTlV2nvqcwuRrMs61rIjycB6eCngxjU9e7Siwn1VjG+FN4pcj2Z5n0VxpbaoO3YKYY0Br0fR+E9rSVa7mJ6aNvWLOk72CoboNNqW9OUpSND/Zn5luVJPjcyGcHUyAoBX5nM0EjbhtEhXrm8RUnyjGaBHB6KxJKEVb87di5difmIw2OIATbM8DXiYb+S6DizJ5fRn5OUy1/kHoQtCfNrykI1k/lYhUmFfq+tNTsvaeP5NEGF1Q/oPmm5o0//Ck34L5VAhLvfbb9KPLJrVcjDZgcKnggbr0wtnmenPVtMDMjQy8ZZFb9o5VQZl+x29pPt8CsMesSuXYiQBV8JuhunfwtZ62y1eu7ntnTWVzlo7i+UO3zhk/wEODa3+ml069mqKl7RrLeW50hdqStkD6pPnmJ0V051ffCh4KGIDge198CC95X25MDd6Ki+QwmqLT8Lcvv5UQ= diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3903d5b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Jacobi Petrucciani + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8719a49 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +repo = pybugsnag +base_command = pytest +coverage = --cov-config setup.cfg --cov=$(repo) +with_report = --cov-report html +term_report = --cov-report term +xml_report = --cov-report xml +reports = $(html_report) $(term_report) $(xml_report) + +all: test_all + +test_all: + $(base_command) $(coverage) $(reports) -s --pyargs $(repo) + + +.PHONY: test_all \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..f7ec640 --- /dev/null +++ b/README.rst @@ -0,0 +1,41 @@ + +pybugsnag +========= + + +.. image:: https://badge.fury.io/py/pybugsnag.svg + :target: https://badge.fury.io/py/pybugsnag + :alt: PyPI version + + +.. image:: https://travis-ci.org/jpetrucciani/pybugsnag.svg?branch=master + :target: https://travis-ci.org/jpetrucciani/pybugsnag + :alt: Build Status + + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/ambv/black + :alt: Code style: black + + +A python wrapper for the Bugsnag Data Access API + +COMING SOON! + +Quick start +----------- + +Installation +^^^^^^^^^^^^ + +.. code-block:: bash + + # install pybugsnag + pip install pybugsnag + +Basic Usage +^^^^^^^^^^^ + +.. code-block:: python + + import pybugsnag diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..c50b005 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,4 @@ +colorama==0.3.7 +pytest==3.5.1 +pytest-cov==2.5.1 +coveralls==1.5.0 diff --git a/pybugsnag/__init__.py b/pybugsnag/__init__.py new file mode 100644 index 0000000..5a18eb8 --- /dev/null +++ b/pybugsnag/__init__.py @@ -0,0 +1,4 @@ +""" +pybugsnag main entrypoint for the library +""" +from pybugsnag.models.client import BugsnagDataClient # noqa diff --git a/pybugsnag/globals.py b/pybugsnag/globals.py new file mode 100644 index 0000000..1dec34a --- /dev/null +++ b/pybugsnag/globals.py @@ -0,0 +1,14 @@ +""" +globals +""" + + +__version__ = "0.0.1" + + +LIBRARY = "pybugsnag" +API_URL = "https://api.bugsnag.com/" + + +TEST_API_URL = "https://private-anon-3633b611b0-bugsnagapiv2.apiary-mock.com/" +TEST_TOKEN = "access_token" diff --git a/pybugsnag/models/__init__.py b/pybugsnag/models/__init__.py new file mode 100644 index 0000000..d04c91b --- /dev/null +++ b/pybugsnag/models/__init__.py @@ -0,0 +1,107 @@ +""" +models for each object in the bugsnag data access api +""" +import json +from pybugsnag.globals import LIBRARY +from pybugsnag.utils.text import filter_locals, snakeify + + +class BaseModel: + """basic model that just parses camelCase json to snake_case keys""" + + def __init__(self, data, client=None, **kwargs): + """constructor""" + self._data = {**data, **kwargs} + self._json = self._jsond(data) + self._client = client + + for key in self._data: + setattr(self, snakeify(key), self._data[key]) + + def _jsond(self, json_data): + """json dumps""" + return json.dumps(json_data) + + def _jsonl(self, dictionary): + """json loads""" + return json.loads(dictionary) + + +class User(BaseModel): + """user object""" + + def __init__(self, data, **kwargs): + """override""" + super(User, self).__init__(data, **kwargs) + + def __repr__(self): + """repr""" + return "<{}.User[{}] '{}'>".format(LIBRARY, self.id, self.email) + + +class Organization(BaseModel): + """Organization object""" + + def __init__(self, data, **kwargs): + """override""" + super(Organization, self).__init__(data, **kwargs) + self.creator = User(self.creator) + self._projects = None + + def __repr__(self): + """repr""" + return "<{}.Organization[{}] '{}'>".format(LIBRARY, self.id, self.name) + + @property + def projects(self): + """cachable projects property""" + if not self._projects or not self._client.cache: + self._projects = self.get_projects() + return self._projects + + def get_projects(self, sort="created_at", direction="desc", per_page=30): + """gets the projects based on the params""" + path = "organizations/{}/projects?sort={}&direction={}&per_page={}".format( + self.id, sort, direction, per_page + ) + return [Project(x) for x in self._client.get(path)] + + +class Project(BaseModel): + """Project object""" + + def __init__(self, data, **kwargs): + """override""" + super(Project, self).__init__(data, **kwargs) + + def __repr__(self): + """repr""" + return "<{}.Project[{}] '{}'>".format(LIBRARY, self.id, self.name) + + def get_errors( + self, + base=None, + sort=None, + direction=None, + per_page=None, + filters=None, + **kwargs + ): + """get errors for this project""" + params = filter_locals(locals()) + + print(params) + + path = "projects/{}/errors?" + + +class Error(BaseModel): + """Error object""" + + def __init__(self, data, **kwargs): + """override""" + super(Error, self).__init__(data, **kwargs) + + def __repr__(self): + """repr""" + return "<{}.Error[{}] '{}'>".format(LIBRARY, self.id, self.error_class) diff --git a/pybugsnag/models/client.py b/pybugsnag/models/client.py new file mode 100644 index 0000000..dc8e0f8 --- /dev/null +++ b/pybugsnag/models/client.py @@ -0,0 +1,76 @@ +""" +base client model to create and use http endpoints +""" +import requests +import urllib.parse +from pybugsnag.globals import __version__, API_URL, LIBRARY, TEST_TOKEN, TEST_API_URL +from pybugsnag.models.error import RateLimited +from pybugsnag.models import Organization, User + + +def test_client(): + """returns a test client""" + return BugsnagDataClient(TEST_TOKEN, api_url=TEST_API_URL, debug=True) + + +class BugsnagDataClient: + """client http wrapper""" + + def __init__(self, token, api_url=API_URL, cache=True, debug=False): + """creates a new client""" + if not token: + raise Exception("no token specified!") + self.token = token + self.api_url = api_url + self.version = __version__ + self.cache = cache + self.debug = debug + + # cache + self._organizations = None + + @property + def headers(self): + """forms the headers required for the API calls""" + return { + "Accept": "application/json; version=2", + "AcceptEncoding": "gzip, deflate", + "Authorization": "token {}".format(self.token), + "User-Agent": "{}/{}".format(LIBRARY, self.version), + } + + def _log(self, *args): + """logging method""" + if not self.debug: + return + print(*args) + + def _req(self, path, method="get", **kwargs): + """requests wrapper""" + full_path = urllib.parse.urljoin(self.api_url, path) + self._log("[{}]: {}".format(method.upper(), full_path)) + request = requests.request(method, full_path, headers=self.headers, **kwargs) + if request.status_code == 429: + raise RateLimited() + return request + + def get(self, path, **kwargs): + """makes a get request to the API""" + return self._req(path, **kwargs).json() + + def post(self, path, **kwargs): + """makes a post request to the API""" + return self._req(path, method="post", **kwargs).json() + + def put(self, path, **kwargs): + """makes a put request to the API""" + return self._req(path, method="put", **kwargs).json() + + @property + def organizations(self): + """organizations list for this access token""" + if not self._organizations or not self.cache: + self._organizations = [ + Organization(x, client=self) for x in self.get("user/organizations") + ] + return self._organizations diff --git a/pybugsnag/models/error.py b/pybugsnag/models/error.py new file mode 100644 index 0000000..e044567 --- /dev/null +++ b/pybugsnag/models/error.py @@ -0,0 +1,22 @@ +""" +error models for pybugsnag +""" + + +class PyBugsnagException(Exception): + """base pybugsnag exception class""" + + def __init__(self, *args, **kwargs): + extra = "" + if args: + extra = '\n| extra info: "{extra}"'.format(extra=args[0]) + print( + "[{exception}]: {doc}{extra}".format( + exception=self.__class__.__name__, doc=self.__doc__, extra=extra + ) + ) + Exception.__init__(self, *args, **kwargs) + + +class RateLimited(PyBugsnagException): + """request received a 429 - you are currently rate limited""" diff --git a/pybugsnag/test/__init__.py b/pybugsnag/test/__init__.py new file mode 100644 index 0000000..9c56114 --- /dev/null +++ b/pybugsnag/test/__init__.py @@ -0,0 +1,3 @@ +""" +pytest files +""" diff --git a/pybugsnag/test/conftest.py b/pybugsnag/test/conftest.py new file mode 100644 index 0000000..9db09f0 --- /dev/null +++ b/pybugsnag/test/conftest.py @@ -0,0 +1,17 @@ +""" +configure pytest +""" +import pytest +from pybugsnag.test.helpers import dbg + + +@pytest.fixture(scope="session", autouse=True) +def before_all(request): + """test setup""" + dbg("[+] begin pybugsnag tests") + request.addfinalizer(after_all) + + +def after_all(): + """tear down""" + dbg("[+] end pybugsnag tests") diff --git a/pybugsnag/test/helpers.py b/pybugsnag/test/helpers.py new file mode 100644 index 0000000..c16e079 --- /dev/null +++ b/pybugsnag/test/helpers.py @@ -0,0 +1,28 @@ +""" +helpers for the pytest suite +""" +import json +import sys +from colorama import init, Fore, Style + + +init() + + +def manual_raise(): + """manually raise an exception""" + raise SystemExit(1) + + +def dbg(text): + """debug printer for tests""" + if isinstance(text, dict): + text = json.dumps(text, sort_keys=True, indent=2) + caller = sys._getframe(1) + print("") + print(Fore.GREEN + Style.BRIGHT) + print("----- {} line {} ------".format(caller.f_code.co_name, caller.f_lineno)) + print(text) + print("-----") + print(Style.RESET_ALL) + print("") diff --git a/pybugsnag/test/test_pybugsnag.py b/pybugsnag/test/test_pybugsnag.py new file mode 100644 index 0000000..12ad815 --- /dev/null +++ b/pybugsnag/test/test_pybugsnag.py @@ -0,0 +1,7 @@ +""" +a base test suite for pybugsnag +""" +from pybugsnag.models.client import test_client + + +CLIENT = test_client() diff --git a/pybugsnag/utils/__init__.py b/pybugsnag/utils/__init__.py new file mode 100644 index 0000000..32f157e --- /dev/null +++ b/pybugsnag/utils/__init__.py @@ -0,0 +1,3 @@ +""" +utilities for pybugsnag +""" diff --git a/pybugsnag/utils/text.py b/pybugsnag/utils/text.py new file mode 100644 index 0000000..bbe2fb4 --- /dev/null +++ b/pybugsnag/utils/text.py @@ -0,0 +1,38 @@ +""" +text manipulation utilities +""" +import datetime +import re + + +FIRST_CAP = re.compile("(.)([A-Z][a-z]+)") +ALL_CAP = re.compile("([a-z0-9])([A-Z])") +LOCALS_FILTER = ["self", "kwargs"] + + +def snakeify(text): + """camelCase to snake_case""" + first_string = FIRST_CAP.sub(r"\1_\2", text) + return ALL_CAP.sub(r"\1_\2", first_string).lower() + + +def ts_to_datetime(timestamp): + """converts the posix x1000 timestamp to a python datetime""" + return datetime.datetime.utcfromtimestamp(int(timestamp) / 1000) + + +def datetime_to_ts(date_object): + """converts a datetime to a posix x1000 timestamp""" + return int(date_object.timestamp() * 1000) + + +def filter_locals(local_variables, extras=None): + """filters out builtin variables in the local scope and returns locals as a dict""" + var_filter = LOCALS_FILTER.copy() + if extras and isinstance(extras, list): + var_filter += extras + return { + x: local_variables[x] + for x in local_variables + if local_variables[x] is not None and x not in var_filter + } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..33a183c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[mypy] +python_version = 3.5 +disallow_untyped_defs = False +ignore_missing_imports = True + +[flake8] +ignore = N802,N807,W503 +max-line-length = 100 +max-complexity = 20 + +[tool:pytest] +log_print = False diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..49f89fa --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +""" +pip setup file +""" +from pybugsnag.globals import __version__, LIBRARY +from setuptools import setup, find_packages + + +with open("README.rst") as readme: + long_description = readme.read() + + +setup( + name=LIBRARY, + version=__version__, + description="A python wrapper for the Bugsnag Data Access API", + long_description=long_description, + author="Jacobi Petrucciani", + author_email="jacobi@mimirhq.com", + keywords="bugsnag python data api", + url="https://github.com/jpetrucciani/{}.git".format(LIBRARY), + download_url="https://github.com/jpetrucciani/{}.git".format(LIBRARY), + license="LICENSE", + packages=find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + ], + zip_safe=False, +)