Skip to content
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 13 commits into from
Jan 17, 2018
Merged
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
*.sqlite
*.swp
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
69 changes: 69 additions & 0 deletions config.py
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
Copy link
Member

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?

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
}
19 changes: 19 additions & 0 deletions dataservice/__init__.py
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
49 changes: 49 additions & 0 deletions dataservice/api/README.md
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
}
```
30 changes: 30 additions & 0 deletions dataservice/api/__init__.py
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
21 changes: 21 additions & 0 deletions dataservice/api/person/README.md
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.
1 change: 1 addition & 0 deletions dataservice/api/person/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .person import person_api
137 changes: 137 additions & 0 deletions dataservice/api/person/person.py
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)
5 changes: 5 additions & 0 deletions dataservice/id_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import uuid


def assign_id():
return str(uuid.uuid4())
Loading