Skip to content

Commit

Permalink
initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
PPB InfoSec Engineering committed Jan 17, 2022
0 parents commit a40b1fc
Show file tree
Hide file tree
Showing 25 changed files with 612 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# file set with Dockerfile.tests (for run_tests script) in mind - review if used for anything else
.jenkins
**/.coverage
**/__pycache__
**/*.pyc
dist
*.egg-info
.git
.reports
.dockerignore
**/.pytest_cache
**/.vscode
29 changes: 29 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: publish

on:
release:
types: [created]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
29 changes: 29 additions & 0 deletions .github/workflows/publish_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: publish test

on:
push:
branches: testpypi

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TEST_TOKEN }}
run: |
python setup.py sdist bdist_wheel
twine upload --repository testpypi dist/*
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.pyc
__pycache__
/dist
*.egg-info
.coverage
.reports
.pytest_cache
.vscode
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
MIT License

Copyright (c) 2021 PaddyPowerBetfair

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.

2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include README.md
include LICENSE
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.PHONY: style
style:
black --target-version=py36 \
--line-length=120 \
--skip-string-normalization \
logbasecommand testapp setup.py

.PHONY: style_check
style_check:
black --target-version=py36 \
--line-length=120 \
--skip-string-normalization \
--check \
logbasecommand testapp setup.py

test:
testapp/manage.py test $${TEST_ARGS:-tests}

coverage:
PYTHONPATH="testapp" \
python -b -W always -m coverage run testapp/manage.py test $${TEST_ARGS:-tests}
coverage report
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# django-logbasecommand

Minimal package to add logging helpers to Django management commands

## Usage

Replace

```
from django.core.management.base import BaseCommand
class YourCommand(BaseCommand):
```

with

```
from logbasecommand.base import LogBaseCommand
class LogBaseCommand(BaseCommand):
```

and now you can use the drop-in methods to replace `self.stdout`/`self.stderr`:
* `log`
* `log_debug`
* `log_warning`
* `log_error`
* `log_exception`

Or access `self.logger` directly.


All command logger names are derived from the command module name and prefixed with `logbasecommand.base` (by default, use `LOGBASECOMMAND_PREFIX` setting to change it), so logging can be configured from your project settings.py, with `LOGGING`, ie (full example, check the `loggers` part):

```python
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {'()': 'django.utils.log.RequireDebugFalse'},
'require_debug_true': {'()': 'django.utils.log.RequireDebugTrue'},
},
'formatters': {
'verbose': {'format': '[%(asctime)s] [%(process)s] [%(levelname)s] [%(name)s] %(message)s'},
'minimal': {'format': '[%(levelname)s] [%(name)s] %(message)s'},
},
'handlers': {
'console': {
'level': 'INFO',
'filters': ['require_debug_false'],
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
'console_debug': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
'console_minimal': {
'level': 'INFO',
'filters': ['require_debug_false'],
'class': 'logging.StreamHandler',
'formatter': 'minimal',
},
'console_debug_minimal': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'minimal',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler',
},
},
'loggers': {
'': {'handlers': ['console', 'console_debug'], 'level': 'INFO', 'propagate': True},
'logbasecommand.base': {
'handlers': ['console_minimal', 'console_debug_minimal'],
'level': 'DEBUG',
'propagate': False,
},
},
}
```
1 change: 1 addition & 0 deletions logbasecommand/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = '0.0.1'
64 changes: 64 additions & 0 deletions logbasecommand/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import logging

from django.core.management.base import (
BaseCommand,
CommandError, # noqa - to make it on subclasses to import from only one place
)
from django.conf import settings


class LogBaseCommand(BaseCommand):
verbosity = 1
custom_stdout = False
custom_stderr = False

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
prefix = getattr(settings, 'LOGBASECOMMAND_PREFIX', None) or __name__
self.logger = logging.getLogger(prefix + '.' + self.__module__.split('.')[-1])

def __handle_custom_std(self, ifstd, std, msg, *args):
if ifstd:
std.write(str(msg) if not args else msg % args)

def __custom_stderr(self, msg, *args):
self.__handle_custom_std(self.custom_stderr, self.stderr, msg, *args)

def __custom_stdout(self, msg, *args):
self.__handle_custom_std(self.custom_stdout, self.stdout, msg, *args)

def log_debug(self, msg, *args, **kwargs):
if self.verbosity >= 2:
self.__custom_stdout(msg, *args)
return self.logger.debug(msg, *args, **kwargs)

def log(self, msg, *args, **kwargs):
self.__custom_stdout(msg, *args)
return self.logger.info(msg, *args, **kwargs)

def log_warning(self, msg, *args, **kwargs):
self.__custom_stderr(msg, *args)
return self.logger.warning(msg, *args, **kwargs)

def log_error(self, msg, *args, **kwargs):
self.__custom_stderr(msg, *args)
return self.logger.error(msg, *args, **kwargs)

def log_exception(self, msg, *args, **kwargs):
self.__custom_stderr(msg, *args)
return self.logger.exception(msg, *args, **kwargs)

def execute(self, *args, **options):
self.verbosity = options['verbosity']
self.logger.setLevel(
[logging.ERROR, max(self.logger.getEffectiveLevel(), logging.INFO), logging.DEBUG, logging.DEBUG][
self.verbosity
]
)

if options.get('stdout') is not None:
self.custom_stdout = True
if options.get('stderr') is not None:
self.custom_stderr = True

super().execute(*args, **options)
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
DJANGO_SETTINGS_MODULE = testapp.settings
python_files = tests.py test_*.py
41 changes: 41 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[metadata]
name = django-logbasecommand
version = attr: logbasecommand.__version__
description = Minimal package to add logging helpers to Django management commands
author = PPB - InfoSec Engineering
author_email = [email protected]
url = https://github.com/surface-security/django-logbasecommand/
long_description = file: README.md
long_description_content_type = text/markdown
license = MIT
classifiers =
Development Status :: 5 - Production/Stable
Framework :: Django
License :: OSI Approved :: MIT License
Intended Audience :: Developers
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 3.9
Environment :: Web Environment
Topic :: Software Development

[options]
zip_safe = False
include_package_data = True
packages = find:
install_requires =
Django >= 3.0, < 4

python_requires = >=3.6

[options.packages.find]
exclude =
tests
tests.*

[coverage:run]
source = logbasecommand

[coverage:report]
show_missing = True
skip_covered = True
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from setuptools import setup

setup()
22 changes: 22 additions & 0 deletions testapp/manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testapp.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)


if __name__ == '__main__':
main()
5 changes: 5 additions & 0 deletions testapp/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-e ..
pytest==6.2.5
pytest-cov==2.12.1
pytest-django==4.4.0
black==20.8b1
Empty file added testapp/testapp/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions testapp/testapp/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
ASGI config for testapp project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testapp.settings')

application = get_asgi_application()
Empty file.
Empty file.
12 changes: 12 additions & 0 deletions testapp/testapp/management/commands/some_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from logbasecommand.base import LogBaseCommand


class Command(LogBaseCommand):
def handle(self, *args, **options):
self.log('info message')
self.log_debug('debug message')
self.log_error('error message')
try:
1 / 0
except ZeroDivisionError:
self.log_exception('exception handled')
Loading

0 comments on commit a40b1fc

Please sign in to comment.