-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ Create person entity #10
Merged
Merged
Changes from 11 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
1c64382
:sparkles: Create person entity
dankolbman 43636aa
📝 Add more detailed docs
dankolbman 9310725
🔧 Update person api with all crud methods
znatty22 da3c6ea
🔧 Update person model and api tests
znatty22 740f251
:card_file_box: Add base mixin for the data model
dankolbman 53b47e2
Merge pull request #23 from kids-first/model-mixins
znatty22 65dd239
Change source_name to external_id
znatty22 fc735ff
🔧 Refractor Person resource classes
znatty22 5ac2dbd
🔧 Change name of id param from person_id to kf_id in person api
znatty22 9d73948
🔧 Update Person resource error handling
znatty22 9026e51
🐛 Fix bug in Person put method
znatty22 be2bd14
Add PyCharm files to gitignore
znatty22 7af27b0
Refactor dataservice to be more modular
znatty22 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
*.sqlite | ||
*.swp | ||
# Byte-compiled / optimized / DLL files | ||
__pycache__/ | ||
*.py[cod] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import os | ||
basedir = os.path.abspath(os.path.dirname(__file__)) | ||
|
||
|
||
class Config: | ||
HOST = "0.0.0.0" | ||
SSL_DISABLE = os.environ.get("SSL_DISABLE", False) | ||
|
||
RESTPLUS_MASK_SWAGGER = False | ||
|
||
@staticmethod | ||
def init_app(app): | ||
pass | ||
|
||
|
||
class DevelopmentConfig(Config): | ||
DEBUG = True | ||
SSL_DISABLE = True | ||
SQLALCHEMY_DATABASE_URI = os.environ.get("DEV_DATABASE_URL") or \ | ||
"sqlite:///" + os.path.join(basedir, "data-dev.sqlite") | ||
SQLALCHEMY_TRACK_MODIFICATIONS = True | ||
|
||
|
||
class TestingConfig(Config): | ||
SERVER_NAME = "localhost" | ||
TESTING = True | ||
WTF_CSRF_ENABLED = False | ||
SQLALCHEMY_DATABASE_URI = os.environ.get("TEST_DATABASE_URL") or \ | ||
"sqlite:///" + os.path.join(basedir, "data-test.sqlite") | ||
SQLALCHEMY_TRACK_MODIFICATIONS = True | ||
|
||
|
||
class ProductionConfig(Config): | ||
# Should use postgres | ||
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") or \ | ||
"sqlite:///" + os.path.join(basedir, "data.sqlite") | ||
|
||
@classmethod | ||
def init_app(cls, app): | ||
Config.init_app(app) | ||
|
||
# email errors to the administrators | ||
import logging | ||
from logging.handlers import SMTPHandler | ||
credentials = None | ||
secure = None | ||
|
||
|
||
class UnixConfig(ProductionConfig): | ||
@classmethod | ||
def init_app(cls, app): | ||
ProductionConfig.init_app(app) | ||
|
||
# log to syslog | ||
import logging | ||
from logging.handlers import SysLogHandler | ||
syslog_handler = SysLogHandler() | ||
syslog_handler.setLevel(logging.WARNING) | ||
app.logger.addHandler(syslog_handler) | ||
|
||
|
||
config = { | ||
"development": DevelopmentConfig, | ||
"testing": TestingConfig, | ||
"production": ProductionConfig, | ||
"unix": UnixConfig, | ||
|
||
"default": DevelopmentConfig | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import os | ||
from flask import Flask, jsonify | ||
from flask_sqlalchemy import SQLAlchemy | ||
from flask_restplus import Api | ||
from config import config, DevelopmentConfig | ||
|
||
db = SQLAlchemy() | ||
|
||
|
||
def create_app(config_name): | ||
app = Flask(__name__) | ||
app.config.from_object(config[config_name]) | ||
config[config_name].init_app(app) | ||
|
||
db.init_app(app) | ||
from dataservice.api import api | ||
api.init_app(app) | ||
|
||
return app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
Primary API for interacting with the Kid's First Data Model. | ||
|
||
## Service-Wide Standards and Functionality | ||
|
||
### Field Masking | ||
|
||
Partial data fetching is supported via the `X-Fields` header. | ||
To specifiy that only some fields are to be returned, include a bracketed, | ||
coma-delimited list under the `X-Fields` header: | ||
|
||
`X-Fields: {kf_id, name}` | ||
|
||
Brackets may be nested to filter on nested fields: | ||
|
||
`X-Fields: {kf_id, name, type{format, extension}}` | ||
|
||
An asterisk may be used to specify all fields: | ||
|
||
`X-Fields: *` | ||
|
||
Or all sub-fields: | ||
|
||
`X-Fields: {kf_id, type{*}}` | ||
|
||
Or all root fields, but with only some sub-fields: | ||
|
||
`X-Fields: {*, type{format}}` | ||
|
||
|
||
|
||
### Pagination | ||
|
||
``` | ||
{ | ||
pages: [ | ||
{ "doc_id": 30, "value": "Lorem" }, | ||
{ "doc_id": 31, "value": "ipsum" }, | ||
{ "doc_id": 32, "value": "dolor" }, | ||
... | ||
{ "doc_id": 40, "value": "amet" } | ||
], | ||
from: 30, | ||
to: 40, | ||
results: 10, | ||
total: 1204, | ||
message: "Success", | ||
status: 200 | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from flask_restplus import Api | ||
from .person import person_api | ||
|
||
api = Api(title='Kids First Data Service', | ||
description=open('dataservice/api/README.md').read(), | ||
version='0.1', | ||
default='', | ||
default_label='') | ||
|
||
api.add_namespace(person_api) | ||
|
||
|
||
@api.documentation | ||
def redoc_ui(): | ||
""" Uses ReDoc for swagger documentation """ | ||
docs_page = """<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>API Docs</title> | ||
<!-- needed for mobile devices --> | ||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
</head> | ||
<body> | ||
<redoc spec-url="{}"></redoc> | ||
<script src="https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"> | ||
</script> | ||
</body> | ||
</html> | ||
""".format(api.specs_url) | ||
return docs_page |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
A person is one of the central entities of the Kid's First DCC. | ||
|
||
### Fields | ||
|
||
`kf_id` - the unique identifier assigned by Kid's First used as the primary | ||
reference to this Person | ||
|
||
`source_name` - the identifier used by the original contributor of the data | ||
|
||
`date_created` - the date the person`s record was created in the DCC | ||
|
||
`date_modified` - the last date that the record's fields were modified. | ||
Restricted to fields of the entity itself, not any of it's related entities. | ||
|
||
### Identifiers | ||
|
||
The Kid's First DCC assigns a unique, internal identifier of the form: | ||
`KF-P000000` on creation of a new Person. This identifier is used accross the | ||
Kids First Data Service and Data Resource Portal It is expected that the Person | ||
also have an identifier unique to the study it came from. This field is to be | ||
captured in the `source_name` property of the Person upon creation. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .person import person_api |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
from datetime import datetime | ||
from flask import request | ||
from flask_restplus import Namespace, Resource, fields, abort | ||
|
||
from ... import model | ||
from ... import db | ||
|
||
description = open('dataservice/api/person/README.md').read() | ||
|
||
person_api = Namespace(name='persons', description=description) | ||
|
||
person_model = person_api.model('Person', { | ||
'kf_id': fields.String( | ||
example='KF00001', | ||
description='ID assigned by Kids First'), | ||
'created_at': fields.String( | ||
example=datetime.now().isoformat(), | ||
description='Date Person was registered in with the DCC'), | ||
'modified_at': fields.String( | ||
example=datetime.now().isoformat(), | ||
description='Date of last update to the Persons data'), | ||
'external_id': fields.String( | ||
example='SUBJ-3993', | ||
description='Identifier used in the original study data') | ||
}) | ||
|
||
person_list = person_api.model("Persons", { | ||
"persons": fields.List(fields.Nested(person_model)) | ||
}) | ||
|
||
response_model = person_api.model('Response', { | ||
'content': fields.Nested(person_list), | ||
'status': fields.Integer( | ||
description='HTTP response status code', | ||
example=200), | ||
'message': fields.String( | ||
description='Additional information about the response', | ||
example='Success') | ||
}) | ||
|
||
|
||
@person_api.route('/') | ||
class PersonList(Resource): | ||
@person_api.marshal_with(response_model) | ||
def get(self): | ||
""" | ||
Get all persons | ||
""" | ||
persons = model.Person.query.all() | ||
return {'status': 200, | ||
'message': '{} persons'.format(len(persons)), | ||
'content': {'persons': persons}}, 200 | ||
|
||
@person_api.marshal_with(response_model) | ||
@person_api.doc(responses={201: 'person created', | ||
400: 'invalid data'}) | ||
@person_api.expect(person_model) | ||
def post(self): | ||
""" | ||
Create a new person | ||
|
||
Creates a new person and assigns a Kids First id | ||
""" | ||
body = request.json | ||
person = model.Person(**body) | ||
db.session.add(person) | ||
db.session.commit() | ||
return {'status': 201, | ||
'message': 'person created', | ||
'content': {'persons': [person]}}, 201 | ||
|
||
|
||
@person_api.route('/<string:kf_id>') | ||
class Person(Resource): | ||
@person_api.marshal_with(response_model) | ||
@person_api.doc(responses={200: 'person found', | ||
404: 'person not found'}) | ||
def get(self, kf_id): | ||
""" | ||
Get a person by id | ||
Gets a person given a Kids First id | ||
""" | ||
person = model.Person.query.filter_by(kf_id=kf_id).one_or_none() | ||
if not person: | ||
self._not_found(kf_id) | ||
|
||
return {'status': 200, | ||
'message': 'person found', | ||
'content': {'persons': [person]}}, 200 | ||
|
||
@person_api.marshal_with(response_model) | ||
@person_api.doc(responses={201: 'person updated', | ||
400: 'invalid data', | ||
404: 'person not found'}) | ||
@person_api.expect(person_model) | ||
def put(self, kf_id): | ||
""" | ||
Update an existing person | ||
""" | ||
body = request.json | ||
person = model.Person.query.filter_by(kf_id=kf_id).one_or_none() | ||
if not person: | ||
self._not_found(kf_id) | ||
|
||
person.external_id = body.get('external_id') | ||
db.session.commit() | ||
|
||
return {'status': 201, | ||
'message': 'person updated', | ||
'content': {'persons': [person]}}, 201 | ||
|
||
@person_api.marshal_with(response_model) | ||
@person_api.doc(responses={204: 'person deleted', | ||
404: 'person not found'}) | ||
def delete(self, kf_id): | ||
""" | ||
Delete person by id | ||
|
||
Deletes a person given a Kids First id | ||
""" | ||
person = model.Person.query.filter_by(kf_id=kf_id).one_or_none() | ||
if not person: | ||
self._not_found(kf_id) | ||
|
||
db.session.delete(person) | ||
db.session.commit() | ||
return {'status': 200, | ||
'message': 'person deleted', | ||
'content': {'persons': [person]}}, 200 | ||
|
||
def _not_found(self, kf_id): | ||
""" | ||
Temporary helper - will do error handling better later | ||
""" | ||
status = 404 | ||
abort(status, "Person with kf_id '{}' not found".format(kf_id), | ||
status=status, content=None) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import uuid | ||
|
||
|
||
def assign_id(): | ||
return str(uuid.uuid4()) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how emailing errors works, but is this necessary right now?