Add marshmallow schemas and and completed base routes

master
androiddrew 8 years ago
parent a02d696f9f
commit 7285ed4a00

@ -1 +1,2 @@
from .app import application_factory from .app import application_factory
from .app import application_factory

@ -7,14 +7,15 @@ from apistar.handlers import docs_urls, static_urls
from .models import Base, NewsArticle, NewsSource from .models import Base, NewsArticle, NewsSource
from .renders import JSONRenderer from .renders import JSONRenderer
from .schema import NewsSourceSchema from .schema import NewsSourceSchema, NewsArticleSchema
news_source_schema = NewsSourceSchema() news_source_schema = NewsSourceSchema()
news_article_schema = NewsArticleSchema()
def get_sources(session: Session): def get_sources(session: Session):
sources = session.query(NewsSource).all() sources = session.query(NewsSource).all()
return news_source_schema.dump(sources, many=True).data return http.Response(news_source_schema.dump(sources, many=True).data, status=200)
def add_source(session: Session, request_data: http.RequestData, router: Router): def add_source(session: Session, request_data: http.RequestData, router: Router):
@ -54,15 +55,42 @@ def get_source(session: Session, id: int):
def get_articles(session: Session): def get_articles(session: Session):
articles = session.query(NewsArticle).all() articles = session.query(NewsArticle).all()
return [article.to_dict() for article in articles] # return [article.to_dict() for article in articles]
return http.Response(news_article_schema.dump(articles, many=True).data, status=200)
def add_article(session: Session): def add_article(session: Session, request_data: http.RequestData, router: Router):
pass news_article_data, errors = news_article_schema.load(request_data)
if errors:
msg = {"message": "400 Bad Request", "error": errors}
return http.Response(msg, status=400)
news_article = NewsArticle(**news_article_data)
session.add(news_article)
session.flush
headers = {"Location": router.reverse_url('get_source', {"id": news_article.id})}
return http.Response(news_article_schema.dump(news_article), status=201, headers=headers)
def get_article(session: Session, id: int):
news_article = session.query(NewsArticle).filter_by(id=id).one_or_none()
if news_article is None:
msg = {"message": "404 Not Found"}
return http.Response(msg, status=404)
return http.Response(news_article_schema.dump(news_article), status=200)
def delete_article(session: Session): def delete_article(session: Session, id):
pass """Delete a single News Sources from the collection by id"""
news_article = session.query(NewsArticle).filter_by(id=id).one_or_none()
if news_article is None:
msg = {"message": "404 Not Found"}
return http.Response(msg, status=404)
msg = {"message": "200 OK"}
return http.Response(msg, status=200)
routes = [ routes = [
@ -71,6 +99,9 @@ routes = [
Route('/sources/{id}', 'GET', get_source), Route('/sources/{id}', 'GET', get_source),
Route('/sources/{id}', 'DELETE', delete_source), Route('/sources/{id}', 'DELETE', delete_source),
Route('/articles', 'GET', get_articles), Route('/articles', 'GET', get_articles),
Route('/articles', 'POST', add_article),
Route('/articles/{id}', 'GET', get_article),
Route('/articles/{id}', 'DELETE', delete_article),
Include('/docs', docs_urls), Include('/docs', docs_urls),
Include('/static', static_urls) Include('/static', static_urls)
] ]

@ -4,6 +4,7 @@ from sqlalchemy.orm import relationship, backref
from sqlalchemy.sql import expression, and_ from sqlalchemy.sql import expression, and_
from sqlalchemy.ext.compiler import compiles from sqlalchemy.ext.compiler import compiles
from sqlalchemy.types import DateTime as DateTimeType from sqlalchemy.types import DateTime as DateTimeType
from sqlalchemy.orm.exc import NoResultFound
class utcnow(expression.FunctionElement): class utcnow(expression.FunctionElement):
@ -53,7 +54,7 @@ class Tag(DBMixin, Base):
def get_or_create(session, name): def get_or_create(session, name):
try: try:
return session.query(Tag).filter_by(tag_name=name).one() return session.query(Tag).filter_by(tag_name=name).one()
except: except NoResultFound:
return Tag(tag_name=name) return Tag(tag_name=name)
@ -65,7 +66,7 @@ class Category(DBMixin, Base):
def get_or_create(session, name): def get_or_create(session, name):
try: try:
return session.query(Category).filter_by(category_name=name).one() return session.query(Category).filter_by(category_name=name).one()
except: except NoResultFound:
return Category(category_name=name) return Category(category_name=name)
@ -84,7 +85,7 @@ class NewsArticle(DBMixin, Base):
url = Column(Text, unique=True) url = Column(Text, unique=True)
title = Column(Text) title = Column(Text)
authors = Column(ARRAY(Text)) authors = Column(ARRAY(Text))
publish_date = Column(DateTime) published_date = Column(DateTime)
news_blob = Column(Text) news_blob = Column(Text)
news_source = relationship('NewsSource', backref=backref('articles', lazy='dynamic')) news_source = relationship('NewsSource', backref=backref('articles', lazy='dynamic'))
tags = relationship('Tag', secondary=tags, backref=backref('articles', lazy='dynamic')) tags = relationship('Tag', secondary=tags, backref=backref('articles', lazy='dynamic'))

@ -1,13 +1,44 @@
import datetime as dt
from marshmallow import Schema, fields from marshmallow import Schema, fields
class PassDateTime(fields.DateTime):
def _deserialize(self, value, attr, data):
if isinstance(value, dt.datetime):
return value
return super()._deserialize(value, attr, data)
class NewsSourceSchema(Schema): class NewsSourceSchema(Schema):
id = fields.Int(dump_only=True) id = fields.Int(dump_only=True)
created_date = fields.DateTime(dump_only=True) created_date = fields.DateTime(dump_only=True)
modified_date = fields.DateTime() modified_date = fields.DateTime(dump_only=True)
url = fields.URL() url = fields.URL()
source_name = fields.Str(required=True, error_messages={'required': 'NewsSource name is a required field'}) source_name = fields.Str(required=True, error_messages={'required': 'NewsSource name is a required field'})
source_type = fields.Str(required=True, error_messages={'required': 'NewsSource tyoe is a required field'}) source_type = fields.Str(required=True, error_messages={'required': 'NewsSource type is a required field'})
# TODO add support for Categories
class Meta:
ordered = True
class NewsArticleSchema(Schema):
id = fields.Int(dump_only=True)
created_date = fields.DateTime(dump_only=True)
modified_date = fields.DateTime(dump_only=True)
news_source_id = fields.Int(required=True, error_messages={
'required': 'A NewsArticle must include a NewsSource identified by NewsSource.id'}
)
url = fields.URL(required=True, error_messages={'required': 'A NewsArticle must include a URL'})
title = fields.Str()
authors = fields.List(fields.Str())
# published_date = fields.DateTime()
published_date = PassDateTime()
news_blob = fields.Str(required=True, error_messages={'required': 'NewsArticle must include news content'})
# TODO add support for Tags
class Meta: class Meta:
ordered = True ordered = True

@ -1,3 +1,5 @@
from unittest import mock
from apistar.frameworks.wsgi import WSGIApp from apistar.frameworks.wsgi import WSGIApp
from apistar.backends.sqlalchemy_backend import SQLAlchemyBackend, get_session from apistar.backends.sqlalchemy_backend import SQLAlchemyBackend, get_session
@ -49,3 +51,14 @@ def apistar_app_fixture():
commands=commands, commands=commands,
components=components, components=components,
routes=routes) routes=routes)
@pytest.fixture(name='router', scope="session")
def apistar_router_fixture():
"""Returns a session scoped mock Router"""
mock_router = mock.Mock()
mock_router.configure_mock(
**{"reverse_url.return_value": "/mylocation"}
)
return mock_router

@ -3,14 +3,17 @@ from unittest import mock
import pytest import pytest
from news.app import get_articles, get_sources, add_source, get_source, delete_source from news.app import get_articles, get_sources, add_source, get_source, delete_source, add_article, get_article, \
delete_article
from news.models import NewsArticle, NewsSource, Category, Tag from news.models import NewsArticle, NewsSource, Category, Tag
# Sources # Sources
def test_get_empty__news_sources(rb_session): def test_get_empty__news_sources(rb_session):
assert [] == get_sources(rb_session) result = get_sources(rb_session)
assert 200 == result.status
assert [] == result.content
def test_get_news_sources(rb_session): def test_get_news_sources(rb_session):
@ -22,44 +25,36 @@ def test_get_news_sources(rb_session):
rb_session.add(test_source) rb_session.add(test_source)
rb_session.flush() rb_session.flush()
assert 1 == len(get_sources(rb_session)) result = get_sources(rb_session)
assert 200 == result.status
assert 1 == len(result.content)
def test_add_news_source(rb_session): def test_add_news_source(rb_session, router):
test_source = { test_source = {
'source_name': 'TEST', 'source_name': 'TEST',
'source_type': 'website', 'source_type': 'website',
'url': 'http://money.test.com' 'url': 'http://money.test.com'
} }
mock_router = mock.Mock() result = add_source(rb_session, test_source, router)
mock_router.configure_mock(
**{"reverse_url.return_value": "/mylocation"}
)
result = add_source(rb_session, test_source, mock_router)
assert 201 == result.status assert 201 == result.status
assert 'location' in result.headers.keys() assert 'location' in result.headers.keys()
def test_add_news_source_error(rb_session): def test_add_news_source_error(rb_session, router):
"""Testing with missing required field""" """Testing with missing required field"""
test_source = { test_source = {
'source_type': 'website', 'source_type': 'website',
'url': 'http://money.test.com' 'url': 'http://money.test.com'
} }
mock_router = mock.Mock() result = add_source(rb_session, test_source, router)
mock_router.configure_mock(
**{"reverse_url.return_value": "/mylocation"}
)
result = add_source(rb_session, test_source, mock_router)
print(result.headers) print(result.headers)
assert 400 == result.status assert 400 == result.status
def test_get_news_source(rb_session): def test_get_news_source_by_id(rb_session):
test_source = NewsSource(url='http://money.test.com', test_source = NewsSource(url='http://money.test.com',
source_name='TEST', source_name='TEST',
source_type='website', source_type='website',
@ -117,11 +112,133 @@ def test_delete_news_source_404(rb_session):
# Articles # Articles
def test_get_empty_articles(rb_session): def test_get_empty_news_articles(rb_session):
assert get_articles(rb_session) == [] result = get_articles(rb_session)
assert 200 == result.status
assert [] == result.content
def test_get_news_articles(rb_session):
source = NewsSource(url='http://money.test.com',
source_name='TEST',
source_type='website',
categories=[Category(category_name='finance')]
)
article = NewsArticle(url='http://money.test.com/article',
title='article',
authors=['drew', 'jesse'],
published_date=dt.datetime.utcnow(),
news_blob='article content',
news_source=source,
tags=[Tag(tag_name='article')]
)
rb_session.add(article)
rb_session.flush()
result = get_articles(rb_session)
assert 200 == result.status
assert 1 == len(result.content)
def test_add_news_article(rb_session, router):
news_source = NewsSource(url='http://money.test.com',
source_name='TEST',
source_type='website',
categories=[Category(category_name='finance')]
)
rb_session.add(news_source)
rb_session.flush()
news_article_data = {
"news_source_id": 1,
"url": "http://money.test.com/article",
"title": "article",
"authors": ["drew", "jesse"],
"published_date": dt.datetime.utcnow(),
"news_blob": "Here's all the great content"
}
result = add_article(rb_session, news_article_data, router)
assert 201 == result.status
def test_add_news_article_error(rb_session, router):
"""Testing with missing required field"""
news_source = NewsSource(url='http://money.test.com',
source_name='TEST',
source_type='website',
categories=[Category(category_name='finance')]
)
rb_session.add(news_source)
rb_session.flush()
news_article_data = {
"news_source_id": 1,
"title": "article",
"authors": ["drew", "jesse"],
"published_date": dt.datetime.utcnow(),
"news_blob": "Here's all the great content"
}
result = add_article(rb_session, news_article_data, router)
assert 400 == result.status
def test_get_article_by_id(rb_session):
source = NewsSource(url='http://money.test.com',
source_name='TEST',
source_type='website',
categories=[Category(category_name='finance')]
)
article = NewsArticle(url='http://money.test.com/article',
title='article',
authors=['drew', 'jesse'],
published_date=dt.datetime.utcnow(),
news_blob='article content',
news_source=source,
tags=[Tag(tag_name='article')]
)
rb_session.add(article)
rb_session.flush()
result = get_article(rb_session, 1)
assert 200 == result.status
def test_delete_article_by_id(rb_session):
source = NewsSource(url='http://money.test.com',
source_name='TEST',
source_type='website',
categories=[Category(category_name='finance')]
)
article = NewsArticle(url='http://money.test.com/article',
title='article',
authors=['drew', 'jesse'],
published_date=dt.datetime.utcnow(),
news_blob='article content',
news_source=source,
tags=[Tag(tag_name='article')]
)
rb_session.add(article)
rb_session.flush()
result = delete_article(rb_session, 1)
assert 200 == result.status
def test_get_articles(rb_session): def test_delete_article_404(rb_session):
source = NewsSource(url='http://money.test.com', source = NewsSource(url='http://money.test.com',
source_name='TEST', source_name='TEST',
source_type='website', source_type='website',
@ -131,7 +248,7 @@ def test_get_articles(rb_session):
article = NewsArticle(url='http://money.test.com/article', article = NewsArticle(url='http://money.test.com/article',
title='article', title='article',
authors=['drew', 'jesse'], authors=['drew', 'jesse'],
publish_date=dt.datetime.utcnow(), published_date=dt.datetime.utcnow(),
news_blob='article content', news_blob='article content',
news_source=source, news_source=source,
tags=[Tag(tag_name='article')] tags=[Tag(tag_name='article')]
@ -140,4 +257,6 @@ def test_get_articles(rb_session):
rb_session.add(article) rb_session.add(article)
rb_session.flush() rb_session.flush()
assert 1 == len(get_articles(rb_session)) result = delete_article(rb_session, 2)
assert 404 == result.status

Loading…
Cancel
Save