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 #128 from lendingblock/master
Browse files Browse the repository at this point in the history
Release 0.9.0
  • Loading branch information
markharley authored Oct 4, 2018
2 parents edfcb9f + 217329b commit 194132a
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 9 deletions.
2 changes: 1 addition & 1 deletion openapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Minimal OpenAPI asynchronous server application
"""

__version__ = '0.8.9'
__version__ = '0.9.0'
2 changes: 1 addition & 1 deletion openapi/data/dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


def is_nothing(value):
if value is 0 or value is False:
if value == 0 or value is False:
return False
return not value

Expand Down
23 changes: 23 additions & 0 deletions openapi/db/dbmodel.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from sqlalchemy import or_
from sqlalchemy.sql import and_, Select

from ..db.container import Database
Expand All @@ -17,6 +18,18 @@ def get_order_clause(cls, table, query, order_by, order_desc):
order_by_column = order_by_column.desc()
return query.order_by(order_by_column)

@classmethod
def get_search_clause(cls, table, query, search, search_columns):
if not search:
return query

columns = [getattr(table.c, col) for col in search_columns]
return query.where(
or_(
*(col.ilike(f'%{search}%') for col in columns)
)
)

async def db_select(self, table, filters, *, conn=None, consumer=None):
query = self.get_query(table, table.select(), consumer, filters)
sql, args = compile_query(query)
Expand Down Expand Up @@ -48,6 +61,8 @@ def get_query(self, table, query, consumer=None, params=None):
offset = params.pop('offset', 0)
order_by = params.pop('order_by', None)
order_desc = params.pop('order_desc', False)
search = params.pop('search', None)
search_columns = params.pop('search_fields', [])
for key, value in params.items():
bits = key.split(':')
field = bits[0]
Expand All @@ -74,6 +89,14 @@ def get_query(self, table, query, consumer=None, params=None):
query = query.offset(offset)
query = query.limit(limit)

# search
query = self.get_search_clause(
table,
query,
search,
search_columns
)

return query

def default_filter_field(self, field, op, value):
Expand Down
24 changes: 20 additions & 4 deletions openapi/rest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from dataclasses import dataclass
import typing

from .data.fields import Choice, IntegerValidator
from dataclasses import dataclass

from .cli import OpenApiClient
from .data.fields import data_field, bool_field
from .data.fields import (
Choice, IntegerValidator, bool_field, data_field,
str_field,
)
from .spec import OpenApi, OpenApiSpec
from .spec.utils import docjoin
from .spec.pagination import MAX_PAGINATION_LIMIT
from .spec.utils import docjoin


def rest(
Expand Down Expand Up @@ -63,3 +66,16 @@ class Orderable:
)
)
return Orderable


def searchable(*searchable_fields):
@dataclass
class Searchable:
search_fields = list(searchable_fields)
search: str = str_field(
description=(
'Search query string'
),
required=False
)
return Searchable
6 changes: 6 additions & 0 deletions openapi/spec/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ def cleaned(
elif schema == 'path_schema':
raise web.HTTPNotFound()
self.raiseValidationError(errors=validated.errors)

# Hacky hacky hack hack
# Later we'll want to implement proper multicolumn search and so
# this will be removed and will be included directly in the schema
if hasattr(Schema, 'search_fields'):
validated.data['search_fields'] = Schema.search_fields
return validated.data

def dump(self, schema, data):
Expand Down
2 changes: 1 addition & 1 deletion tests/example/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def meta(meta=None):
sa.Column('severity', sa.Integer),
sa.Column('type', sa.Enum(TaskType)),
sa.Column('unique_title', sa.String, nullable=True, unique=True),
sa.Column('story_points', sa.Numeric(2)),
sa.Column('story_points', sa.Numeric),
sa.Column('random', sa.String(64))
)

Expand Down
9 changes: 7 additions & 2 deletions tests/example/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
data_field, date_time_field, decimal_field, enum_field, integer_field,
uuid_field,
)
from openapi.rest import Query, orderable
from openapi.rest import Query, orderable, searchable


class TaskType(enum.Enum):
Expand Down Expand Up @@ -55,7 +55,12 @@ class TaskQuery:


@dataclass
class TaskOrderableQuery(TaskQuery, orderable('title'), Query):
class TaskOrderableQuery(
TaskQuery,
orderable('title'),
searchable('title', 'unique_title'),
Query
):
pass


Expand Down
19 changes: 19 additions & 0 deletions tests/test_db.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import os
import uuid
from datetime import datetime
from decimal import Decimal

from click.testing import CliRunner

from openapi.json import dumps
from openapi.testing import jsonBody, equal_dict
from openapi.utils import error_dict

Expand Down Expand Up @@ -368,3 +370,20 @@ async def test_update_unique_error(cli):
)
duplicated_body = await jsonBody(duplicated_response, status=422)
assert duplicated_body['message'] == 'unique_title already exists'


async def test_decimal_zero_returned(cli):
task = {
'title': 'task',
'unique_title': 'task1',
'story_points': Decimal(0)
}
resp = await cli.post('/tasks', data=dumps(task))
body = await jsonBody(resp, status=201)

assert 'story_points' in body

get_resp = await cli.get(f"/tasks/{body['id']}")
get_body = await jsonBody(get_resp, status=200)

assert get_body['story_points'] == Decimal(0)
52 changes: 52 additions & 0 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
tests = [
{
'title': 'test1',
'unique_title': 'thefirsttest',
'severity': 1
},
{
'title': 'test2',
'unique_title': 'anothertest1',
'severity': 3
},
{
Expand Down Expand Up @@ -49,6 +51,7 @@ async def test_spec(test_app):
'title',
'done',
'type',
'search',
'severity',
'severity:lt',
'severity:le',
Expand Down Expand Up @@ -80,3 +83,52 @@ async def test_multiple(cli, fixtures):
test1, test2, test3 = fixtures
params = MultiDict((('severity', 1), ('severity', 3)))
await assert_query(cli, params, [test1, test2])


async def test_search(cli, fixtures):
test1, test2, test3 = fixtures
params = {
'search': 'test',
}
await assert_query(cli, params, [test1, test2, test3])


async def test_search_match_one(cli, fixtures):
test1, test2, test3 = fixtures
params = {
'search': 'est2',
}
await assert_query(cli, params, [test2])


async def test_search_match_one_with_title(cli, fixtures):
test1, test2, test3 = fixtures
params = {
'title': 'test2',
'search': 'est2',
}
await assert_query(cli, params, [test2])


async def test_search_match_none_with_title(cli, fixtures):
params = {
'title': 'test1',
'search': 'est2',
}
await assert_query(cli, params, [])


async def test_search_either_end(cli, fixtures):
test1, test2, test3 = fixtures
params = {
'search': 'est',
}
await assert_query(cli, params, [test1, test2, test3])


async def test_multicolumn_search(cli, fixtures):
test1, test2, test3 = fixtures
params = {
'search': 'est1',
}
await assert_query(cli, params, [test1, test2])

0 comments on commit 194132a

Please sign in to comment.