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 #168 from lendingblock/master
Browse files Browse the repository at this point in the history
1.3.0
  • Loading branch information
lsbardel authored Feb 7, 2019
2 parents 0992d24 + 8b0205c commit c1b594d
Show file tree
Hide file tree
Showing 28 changed files with 577 additions and 387 deletions.
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[run]
source = openapi
omit =
openapi/reload.py
openapi/db/openapi
openapi/_py36.py

[html]
directory = build/coverage/html
Expand Down
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/data/test_validator.py"
"tests/test_db_cli.py"
]
}
]
Expand Down
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.2.5'
__version__ = '1.3.0'
51 changes: 51 additions & 0 deletions openapi/_py36.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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
5 changes: 3 additions & 2 deletions openapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import click
import uvloop

from .utils import get_debug_flag, getLogger
from .utils import get_debug_flag, get_logger
from . import spec


Expand Down Expand Up @@ -61,6 +61,7 @@ def get_serve_app(self):
if self.base_path:
base = web.Application()
base.add_subapp(self.base_path, app)
base['cli'] = self
app = base
return app

Expand Down Expand Up @@ -102,5 +103,5 @@ def serve(ctx, host, port, reload):
"""Run the aiohttp server.
"""
app = ctx.obj['app']['cli'].get_serve_app()
access_log = getLogger()
access_log = get_logger()
web.run_app(app, host=host, port=port, access_log=access_log)
12 changes: 5 additions & 7 deletions openapi/data/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from .. import json
from ..utils import compact_dict

DEFAULT = 'default'
REQUIRED = 'required'
VALIDATOR = 'OPENAPI_VALIDATOR'
DESCRIPTION = 'description'
Expand All @@ -27,32 +26,31 @@ def __init__(self, field, message):


def data_field(
required=False, validator=None, default=None, dump=None, format=None,
description=None, ops=()):
required=False, validator=None, dump=None, format=None,
description=None, ops=(), **kwargs):
"""Extend a dataclass field with
:param validator: optional callable which accept (field, value, data)
as inputs and return the validated value
:param required: boolean specifying if field is required
:param default: optional callable returning the default value
if value is missing
:param dump: optional callable which receive the field value and convert to
the desired value to serve in requests
:param format: optional string which represents the JSON schema format
:param ops: optional tuple of strings specifying available operations
"""
if isinstance(validator, Validator) and not dump:
dump = validator.dump
if 'default_factory' not in kwargs:
kwargs.setdefault('default', None)

f = field(metadata=compact_dict({
VALIDATOR: validator,
REQUIRED: required,
DEFAULT: default,
DUMP: dump,
DESCRIPTION: description,
FORMAT: format,
OPS: ops
}))
}), **kwargs)
return f


Expand Down
29 changes: 22 additions & 7 deletions openapi/data/validate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Dict, List, Tuple
from dataclasses import dataclass
from dataclasses import dataclass, fields, MISSING

from .fields import VALIDATOR, REQUIRED, DEFAULT, ValidationError, field_ops
from .fields import VALIDATOR, REQUIRED, ValidationError, field_ops
from ..utils import mapping_copy, is_subclass


Expand All @@ -24,17 +24,20 @@ def validated_schema(schema, data, *, strict=True):
return schema(**d.data)


def validate(schema, data, *, strict=True, multiple=False):
def validate(
schema, data: Dict, *,
strict: bool = True, multiple: bool = False):
"""Validate a dictionary of data with a given dataclass
"""
errors = {}
cleaned = {}
data = mapping_copy(data)
for field in schema.__dataclass_fields__.values():
for field in fields(schema):
try:
required = field.metadata.get(REQUIRED)
if strict and DEFAULT in field.metadata:
data.setdefault(field.name, field.metadata[DEFAULT])
default = get_default(field)
if strict and default is not None:
data.setdefault(field.name, default)

if field.name not in data and required and strict:
raise ValidationError(field.name, 'required')
Expand Down Expand Up @@ -71,7 +74,7 @@ def validate(schema, data, *, strict=True, multiple=False):


def collect_value(field, name, value):
if value is None or value == 'NULL':
if is_null(value):
return None

validator = field.metadata.get(VALIDATOR)
Expand All @@ -96,3 +99,15 @@ def collect_value(field, name, value):
raise ValidationError(name, 'not a valid value')

return value


def is_null(value):
return value is None or value == 'NULL'


def get_default(field):
if field.default_factory is not MISSING:
value = field.default_factory()
else:
value = field.default
return value if value is not MISSING else None
33 changes: 27 additions & 6 deletions openapi/db/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
import os

from aiohttp.web import Application

from .commands import db
from ..db.dbmodel import CrudDB


def setup_app(app):
store = os.environ.get('DATASTORE')
if not store:
app.logger.warning('DATASTORE not available')
def get_db(
app: Application,
store_url: str = None,
command: bool = True) -> CrudDB:
"""Create an Open API db handler
This function
* add the database to the aiohttp application
* add the db command to the command line client (if command is True)
* add the close handler on shutdown
It returns the database object
"""
store_url = store_url or os.environ.get('DATASTORE')
if not store_url: # pragma: no cover
app.logger.warning('DATASTORE url not available')
else:
app['db'] = CrudDB(store)
app['cli'].add_command(db)
app['db'] = CrudDB(store_url)
app.on_shutdown.append(close_db)
if command:
app['cli'].add_command(db)
return app['db']


async def close_db(app):
await app['db'].close()
66 changes: 48 additions & 18 deletions openapi/db/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ def migration(ctx):
return Migration(ctx.obj['app'])


def get_db(ctx):
return ctx.obj['app']['db']


@click.group()
def db():
"""Perform database migrations."""
"""Perform database migrations and utilities"""
pass


Expand Down Expand Up @@ -70,35 +74,34 @@ def upgrade(ctx, revision, drop_tables):
if drop_tables:
_drop_tables(ctx)
migration(ctx).upgrade(revision)
click.echo(f"upgraded sucessfuly to {revision}")
click.echo(f"upgraded successfully to {revision}")


@db.command()
@click.option('--revision', default='heads')
@click.option('--drop-tables', default=False, is_flag=True,
help="Drop tables before applying migrations")
@click.option('--revision', help='Revision id', required=True)
@click.pass_context
def downgrade(ctx, revision, drop_tables):
def downgrade(ctx, revision):
"""Downgrade to a previous version
"""
if drop_tables:
_drop_tables(ctx)
migration(ctx).downgrade(revision)
click.echo(f"downgraded successfully to {revision}")


def _drop_tables(ctx):
ctx.obj['app']['db'].drop_all_schemas()
click.echo("tables dropped")


@db.command()
@click.option('--revision', default='heads')
@click.pass_context
def show(ctx, revision):
"""Show revision ID and creation date
"""
return migration(ctx).show(revision)
click.echo(migration(ctx).show(revision))


@db.command()
@click.pass_context
def history(ctx):
"""List changeset scripts in chronological order
"""
click.echo(migration(ctx).history())


@db.command()
Expand All @@ -107,9 +110,7 @@ def show(ctx, revision):
def current(ctx, verbose):
"""Show revision ID and creation date
"""
res = migration(ctx).current(verbose)
click.echo(res)
return res
click.echo(migration(ctx).current(verbose))


@db.command()
Expand All @@ -120,7 +121,7 @@ def current(ctx, verbose):
def create(ctx, dbname, force):
"""Creates a new database
"""
engine = ctx.obj['app']['db'].engine
engine = get_db(ctx).engine
url = copy(engine.url)
url.database = dbname
store = str(url)
Expand All @@ -131,3 +132,32 @@ def create(ctx, dbname, force):
return click.echo(f'database {dbname} already available')
create_database(store)
click.echo(f'database {dbname} created')


@db.command()
@click.option(
'--db', default=False, is_flag=True,
help='List tables in database rather than in sqlalchemy metadata')
@click.pass_context
def tables(ctx, db):
"""List all tables managed by the app"""
d = get_db(ctx)
if db:
tables = d.engine.table_names()
else:
tables = d.metadata.tables
for name in sorted(tables):
click.echo(name)


@db.command()
@click.pass_context
def drop(ctx):
"""Drop all tables in database
"""
_drop_tables(ctx)


def _drop_tables(ctx):
get_db(ctx).drop_all_schemas()
click.echo("tables dropped")
Loading

0 comments on commit c1b594d

Please sign in to comment.