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

@ -7,14 +7,15 @@ from apistar.handlers import docs_urls, static_urls
from .models import Base, NewsArticle, NewsSource
from .renders import JSONRenderer
from .schema import NewsSourceSchema
from .schema import NewsSourceSchema, NewsArticleSchema
news_source_schema = NewsSourceSchema()
news_article_schema = NewsArticleSchema()
def get_sources(session: Session):
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):
@ -54,15 +55,42 @@ def get_source(session: Session, id: int):
def get_articles(session: Session):
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):
pass
def add_article(session: Session, request_data: http.RequestData, router: Router):
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):
pass
def delete_article(session: Session, id):
"""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 = [
@ -71,6 +99,9 @@ routes = [
Route('/sources/{id}', 'GET', get_source),
Route('/sources/{id}', 'DELETE', delete_source),
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('/static', static_urls)
]

@ -4,6 +4,7 @@ from sqlalchemy.orm import relationship, backref
from sqlalchemy.sql import expression, and_
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.types import DateTime as DateTimeType
from sqlalchemy.orm.exc import NoResultFound
class utcnow(expression.FunctionElement):
@ -53,7 +54,7 @@ class Tag(DBMixin, Base):
def get_or_create(session, name):
try:
return session.query(Tag).filter_by(tag_name=name).one()
except:
except NoResultFound:
return Tag(tag_name=name)
@ -65,7 +66,7 @@ class Category(DBMixin, Base):
def get_or_create(session, name):
try:
return session.query(Category).filter_by(category_name=name).one()
except:
except NoResultFound:
return Category(category_name=name)
@ -84,7 +85,7 @@ class NewsArticle(DBMixin, Base):
url = Column(Text, unique=True)
title = Column(Text)
authors = Column(ARRAY(Text))
publish_date = Column(DateTime)
published_date = Column(DateTime)
news_blob = Column(Text)
news_source = relationship('NewsSource', 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
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):
id = fields.Int(dump_only=True)
created_date = fields.DateTime(dump_only=True)
modified_date = fields.DateTime()
modified_date = fields.DateTime(dump_only=True)
url = fields.URL()
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:
ordered = True

@ -1,3 +1,5 @@
from unittest import mock
from apistar.frameworks.wsgi import WSGIApp
from apistar.backends.sqlalchemy_backend import SQLAlchemyBackend, get_session
@ -49,3 +51,14 @@ def apistar_app_fixture():
commands=commands,
components=components,
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
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
# Sources
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):
@ -22,44 +25,36 @@ def test_get_news_sources(rb_session):
rb_session.add(test_source)
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 = {
'source_name': 'TEST',
'source_type': 'website',
'url': 'http://money.test.com'
}
mock_router = mock.Mock()
mock_router.configure_mock(
**{"reverse_url.return_value": "/mylocation"}
)
result = add_source(rb_session, test_source, mock_router)
result = add_source(rb_session, test_source, router)
assert 201 == result.status
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"""
test_source = {
'source_type': 'website',
'url': 'http://money.test.com'
}
mock_router = mock.Mock()
mock_router.configure_mock(
**{"reverse_url.return_value": "/mylocation"}
)
result = add_source(rb_session, test_source, mock_router)
result = add_source(rb_session, test_source, router)
print(result.headers)
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',
source_name='TEST',
source_type='website',
@ -117,11 +112,85 @@ def test_delete_news_source_404(rb_session):
# Articles
def test_get_empty_articles(rb_session):
assert get_articles(rb_session) == []
def test_get_empty_news_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_articles(rb_session):
def test_get_article_by_id(rb_session):
source = NewsSource(url='http://money.test.com',
source_name='TEST',
source_type='website',
@ -131,7 +200,7 @@ def test_get_articles(rb_session):
article = NewsArticle(url='http://money.test.com/article',
title='article',
authors=['drew', 'jesse'],
publish_date=dt.datetime.utcnow(),
published_date=dt.datetime.utcnow(),
news_blob='article content',
news_source=source,
tags=[Tag(tag_name='article')]
@ -140,4 +209,54 @@ def test_get_articles(rb_session):
rb_session.add(article)
rb_session.flush()
assert 1 == len(get_articles(rb_session))
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_delete_article_404(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, 2)
assert 404 == result.status

Loading…
Cancel
Save