Skip to content
This repository has been archived by the owner on Mar 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request #169 from lendingblock/master
Browse files Browse the repository at this point in the history
1.3.1 - color logging
  • Loading branch information
lsbardel authored Feb 14, 2019
2 parents c1b594d + 35773cc commit 504569c
Show file tree
Hide file tree
Showing 15 changed files with 159 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"RedirectOutput"
],
"args": [
"tests/test_db_cli.py"
"tests/test_cli.py"
]
}
]
Expand Down
3 changes: 3 additions & 0 deletions dev/requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ pytest-mock
twine
agile-toolkit
asynctest
#
# additional features
colorlog
raven-aiohttp
2 changes: 1 addition & 1 deletion openapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Minimal OpenAPI asynchronous server application"""

__version__ = '1.3.0'
__version__ = '1.3.1'
25 changes: 17 additions & 8 deletions openapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import click
import uvloop

from .utils import get_debug_flag, get_logger
from .utils import get_debug_flag
from .logger import logger, setup_logging
from . import spec


Expand All @@ -18,7 +19,7 @@
class OpenApiClient(click.Group):

def __init__(self, spec, setup_app=None, base_path=None,
commands=None, **extra) -> None:
commands=None, callback=None, **extra) -> None:
params = list(extra.pop('params', None) or ())
self.spec = spec
self.debug = get_debug_flag()
Expand All @@ -34,9 +35,21 @@ def __init__(self, spec, setup_app=None, base_path=None,
is_flag=True,
is_eager=True
),
click.Option(
['-v', '--verbose'],
help='Increase logging verbosity',
is_flag=True,
is_eager=True
),
click.Option(
['-q', '--quiet'],
help='Decrease logging verbosity',
is_flag=True,
is_eager=True
),
)
)
super().__init__(params=params, **extra)
super().__init__(params=params, callback=setup_logging, **extra)
self.add_command(serve)
for command in commands or ():
self.add_command(command)
Expand Down Expand Up @@ -73,10 +86,6 @@ def list_commands(self, ctx):
ctx.obj = dict(app=self.web())
return super().list_commands(ctx)

def main(self, *args, **kwargs):
os.environ['OPENAPI_RUN_FROM_CLI'] = 'true'
return super().main(*args, **kwargs)

def get_server_version(self, ctx, param, value):
if not value or ctx.resilient_parsing:
return
Expand All @@ -103,5 +112,5 @@ def serve(ctx, host, port, reload):
"""Run the aiohttp server.
"""
app = ctx.obj['app']['cli'].get_serve_app()
access_log = get_logger()
access_log = logger if ctx.obj['log_level'] else None
web.run_app(app, host=host, port=port, access_log=access_log)
30 changes: 18 additions & 12 deletions openapi/data/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ def decimal_field(min_value=None, max_value=None, precision=None, **kw):
return data_field(**kw)


def email_field(**kw):
kw.setdefault('validator', email_validator)
def email_field(max_length=None, min_length=None, **kw):
kw.setdefault(
'validator',
EmailValidator(min_length=min_length, max_length=max_length)
)
return data_field(**kw)


Expand Down Expand Up @@ -134,16 +137,6 @@ def json_field(**kw):
# VALIDATORS


def email_validator(field, value, data=None):
value = str(value)
try:
validate_email(value, check_deliverability=False)
except EmailNotValidError:
raise ValidationError(
field.name, '%s not a valid email' % value) from None
return value


class Validator:
dump = None

Expand Down Expand Up @@ -175,6 +168,19 @@ def openapi(self, prop):
prop['maxLength'] = self.max_length


@dataclass
class EmailValidator(StrValidator):

def __call__(self, field, value, data=None):
value = super().__call__(field, value, data=data)
try:
validate_email(value, check_deliverability=False)
except EmailNotValidError:
raise ValidationError(
field.name, '%s not a valid email' % value) from None
return value


class ListValidator(Validator):

def __init__(self, validators):
Expand Down
45 changes: 45 additions & 0 deletions openapi/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import os
import logging

import click

try:
import colorlog
except ImportError: # pragma: no cover
colorlog = None


LEVEL = (os.environ.get('LOG_LEVEL') or 'info').upper()
LOGGER_NAME = os.environ.get('APP_NAME') or 'openapi'
LOG_FORMAT = '%(levelname)s: %(name)s: %(message)s'

logger = logging.getLogger(LOGGER_NAME)


def getLogger(name=None):
if not name:
return logger
return logging.getLogger(f'{LOGGER_NAME}.{name}')


@click.pass_context
def setup_logging(ctx, verbose, quiet):
if verbose:
level = 'DEBUG'
elif quiet:
level = 'ERROR'
else:
level = LEVEL
level = getattr(logging, level) if level != 'NONE' else None
ctx.obj['log_level'] = level
if level:
logger.setLevel(level)
if not logger.hasHandlers():
fmt = LOG_FORMAT
if colorlog:
handler = colorlog.StreamHandler()
fmt = colorlog.ColoredFormatter(f'%(log_color)s{LOG_FORMAT}')
else: # pragma: no cover
handler = logging.StreamHandler()
handler.setFormatter(fmt)
logger.addHandler(handler)
10 changes: 10 additions & 0 deletions openapi/tz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from datetime import datetime

import pytz


UTC = pytz.utc


def utcnow():
return datetime.now(tz=UTC)
12 changes: 0 additions & 12 deletions openapi/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import sys
import logging
from inspect import isclass
from collections import Mapping
from typing import List, Dict
Expand Down Expand Up @@ -58,17 +57,6 @@ def iter_items(data):
return iter(data)


def get_logger():
level = (os.environ.get('LOG_LEVEL') or 'info').upper()
if level != 'NONE':
name = os.environ.get('APP_NAME') or 'openapi'
logger = logging.getLogger(name)
if not logger.hasHandlers():
logger.setLevel(getattr(logging, level))
logger.addHandler(logging.StreamHandler())
return logger


def is_subclass(value, Type):
origin = getattr(value, '__origin__', None) or value
return isclass(origin) and issubclass(origin, Type)
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def requirements(name):


install_requires = requirements('dev/requirements.txt')[0]

tests_require = requirements('dev/requirements-dev.txt')[0]

if sys.version_info < (3, 7):
install_requires.append('dataclasses')
Expand All @@ -48,6 +48,7 @@ def requirements(name):
url='https://github.com/lendingblock/aio-openapi',
python_requires='>=3.6.0',
install_requires=install_requires,
tests_require=tests_require,
include_package_data=True,
classifiers=[
'Development Status :: 3 - Alpha',
Expand Down
2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
import dotenv

dotenv.load_dotenv()

if not os.environ.get('PYTHON_ENV'):
os.environ['PYTHON_ENV'] = 'test'
4 changes: 0 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import os
import shutil

import dotenv

import pytest

from aiohttp import test_utils
Expand All @@ -17,8 +15,6 @@
from . import example


dotenv.load_dotenv()

DEFAULT_DB = 'postgres://postgres:postgres@localhost:5432/openapi'


Expand Down
26 changes: 17 additions & 9 deletions tests/data/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
ValidationError, data_field, bool_field, uuid_field, number_field,
decimal_field, email_field, enum_field, date_time_field,
ListValidator, UUIDValidator, EnumValidator, Choice, DateTimeValidator,
NumberValidator, DecimalValidator, email_validator, BoolValidator,
Validator, IntegerValidator,
NumberValidator, DecimalValidator, EmailValidator, BoolValidator,
Validator, IntegerValidator, VALIDATOR
)


Expand Down Expand Up @@ -129,6 +129,14 @@ def test_DateTimeValidator_dump():
assert validator.dump(value.isoformat()) == value.isoformat()


def test_DateTimeValidator_timezone():
value = datetime.now()
field = date_time_field(timezone=True)
validator = field.metadata[VALIDATOR]
with pytest.raises(ValidationError):
validator(field, value)


def test_NumberValidator_valid():
field = number_field()
validator = NumberValidator(min_value=-10, max_value=10, precision=2)
Expand Down Expand Up @@ -224,21 +232,21 @@ def test_DecimalValidator_dump():

def test_email_validator_valid():
field = email_field()
assert email_validator(field, '[email protected]') == '[email protected]'
assert email_validator(field, '[email protected]') == '[email protected]'
assert email_validator(field, '[email protected]') == '[email protected]'
assert EmailValidator()(field, '[email protected]') == '[email protected]'
assert EmailValidator()(field, '[email protected]') == '[email protected]'
assert EmailValidator()(field, '[email protected]') == '[email protected]'


def test_email_validator_invalid():
field = email_field()
with pytest.raises(ValidationError):
email_validator(field, 'a@email')
EmailValidator()(field, 'a@email')
with pytest.raises(ValidationError):
email_validator(field, 'email.com')
EmailValidator()(field, 'email.com')
with pytest.raises(ValidationError):
email_validator(field, '@email.com')
EmailValidator()(field, '@email.com')
with pytest.raises(ValidationError):
email_validator(field, 1)
EmailValidator()(field, 1)


def test_BoolValidator_valid():
Expand Down
7 changes: 4 additions & 3 deletions tests/example/db.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import uuid
from datetime import datetime, date
from datetime import date

import sqlalchemy as sa
from sqlalchemy_utils import UUIDType

from openapi.db.columns import UUIDColumn
from openapi.data import fields
from openapi.tz import utcnow

from .models import TaskType

Expand Down Expand Up @@ -55,8 +56,8 @@ def meta(meta=None):
default=uuid.uuid4
),
sa.Column('randomdate', sa.Date, nullable=False, default=date.today),
sa.Column('timestamp', sa.DateTime, nullable=False,
default=datetime.now),
sa.Column('timestamp', sa.DateTime(timezone=True),
nullable=False, default=utcnow),
sa.Column('price', sa.Numeric(precision=100, scale=4), nullable=False),
sa.Column('tenor', sa.String(3), nullable=False),
sa.Column('tick', sa.Boolean),
Expand Down
14 changes: 13 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import logging

from unittest.mock import patch

import click
from click.testing import CliRunner

from openapi.rest import rest
from openapi.logger import logger


def test_usage():
Expand All @@ -24,11 +27,20 @@ def test_serve():
runner = CliRunner()
cli = rest(base_path='/v1')
with patch('aiohttp.web.run_app') as mock:
result = runner.invoke(cli, ['serve'])
result = runner.invoke(cli, ['--quiet', 'serve'])
assert result.exit_code == 0
assert mock.call_count == 1
app = mock.call_args[0][0]
assert app.router is not None
assert logger.level == logging.ERROR

with patch('aiohttp.web.run_app') as mock:
result = runner.invoke(cli, ['--verbose', 'serve'])
assert result.exit_code == 0
assert mock.call_count == 1
app = mock.call_args[0][0]
assert app.router is not None
assert logger.level == logging.DEBUG


def test_commands():
Expand Down
26 changes: 26 additions & 0 deletions tests/test_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from unittest.mock import patch

from click.testing import CliRunner

from openapi.rest import rest
from openapi.logger import getLogger


def test_logger():
logger = getLogger()
assert logger.name == 'openapi'
logger = getLogger('foo')
assert logger.name == 'openapi.foo'


def test_serve():
runner = CliRunner()
cli = rest(base_path='/v1')
with patch('aiohttp.web.run_app') as mock:
with patch('openapi.logger.logger.hasHandlers') as hasHandlers:
hasHandlers.return_value = False
with patch('openapi.logger.logger.addHandler') as addHandler:
result = runner.invoke(cli, ['serve'])
assert result.exit_code == 0
assert mock.call_count == 1
assert addHandler.call_count == 1

0 comments on commit 504569c

Please sign in to comment.