Demo Python REST API using Postgres deployed with Kubernetes
This project is a demo API designed for educational and testing purposes only. All patient, encounter, encounter line item, and CPT code data used within this repository is entirely fictional and generated for demonstration purposes.
While the CPT codes included may be real, they are only used as examples. This API is not designed or intended to handle any real patient information or data protected under HIPAA or similar regulations. Please do not use this project for any real-world applications involving sensitive or personally identifiable information.
This is a demo Python REST API using Postgres as its database, deploying with Kubernetes. It's a mock healthcare provider billing system. The database schema has tables for patients, encounters, line items, and Current Procedural Terminology (CPT) codes.
Note that this was developed on a system with the following specs:
- Windows 11
- Docker Desktop 4.34.2
- Docker Desktop Kubernetes 1.30.2
Your mileage may vary a bit with other systems, but it should generally work. :)
Manually install the following before getting started.
- Install Docker Desktop
- Enable Kubernetes in Docker Desktop
- Install Python 3.11
- Install curl to send requests to the API.
Run make install
to install code dependencies.
Run make test
to run unit tests with code coverage output.
Run make registry
to run the Docker Registry locally with Docker. Since this isn't something we'd normally use in a production environment, we'll just run it with Docker locally on port 5000. This enables Kubernetes to find local images. Running make push
will ensure the registry is running.
Run make predeploy
to deploy some things that are needed to run the demo. This installs the ingress-nginx controller to allow ingress to our API running in Kubernetes.
Run make deploy
to deploy a Postgres database as well as the demo API on Kubernetes. This ensures the image is pushed to the local registry and predeploy steps are done.
Use curl
to send requests to the API locally with HTTP on port 80. Here are some example runs:
$ curl -X POST -H "Content-Type: application/json" -d '{"first_name": "John", "last_name": "Oberly"}' http://localhost:80/patients/
{"id":"13d62589-66b8-414f-ba05-7e4fc1c3ac1b","first_name":"John","last_name":"Oberly"}
$ curl -X GET http://localhost:80/patients/
[{"id":"13d62589-66b8-414f-ba05-7e4fc1c3ac1b","first_name":"John","last_name":"Oberly"}]
$ curl -X GET http://localhost:80/patients/13d62589-66b8-414f-ba05-7e4fc1c3ac1b
{"id":"13d62589-66b8-414f-ba05-7e4fc1c3ac1b","first_name":"John","last_name":"Oberly"}
$ curl -X POST -H "Content-Type: application/json" -d '{"date": "2024-01-01"}' http://localhost:80/patients/13d62589-66b8-414f-ba05-7e4fc1c3ac1b/encounters/
{"id":"ffa61c9a-9de2-4bec-8590-d174ff386db7","date":"2024-01-01"}
$ curl -X GET http://localhost:80/patients/13d62589-66b8-414f-ba05-7e4fc1c3ac1b/encounters/
[{"id":"ffa61c9a-9de2-4bec-8590-d174ff386db7","date":"2024-01-01"}]
$ curl -X GET http://localhost:80/patients/13d62589-66b8-414f-ba05-7e4fc1c3ac1b/encounters/ffa61c9a-9de2-4bec-8590-d174ff386db7
{"id":"ffa61c9a-9de2-4bec-8590-d174ff386db7","date":"2024-01-01"}
$ curl -X POST -H "Content-Type: application/json" -d '{"cpt_code": "99213", "units": 0}' http://localhost:80/patients/13d62589-66b8-414f-ba05-7e4fc1c3ac1b/encounters/ffa61c9a-9de2-4bec-8590-d174ff386db7/line_items/
{"cpt_code":"99213","cpt_code_description":"Office or other outpatient visit, established patient, moderate","units":0}
$ curl -X GET -H "Content-Type: application/json" http://localhost:80/patients/13d62589-66b8-414f-ba05-7e4fc1c3ac1b/encounters/ffa61c9a-9de2-4bec-8590-d174ff386db7/line_items/
[{"cpt_code":"99213","cpt_code_description":"Office or other outpatient visit, established patient, moderate","units":0}]
- Patient
- Has a unique identifier chosen at creation
- Has many encounters
- Encounter
- Has one patient
- Has one date
- Has many line items
- Line Item
- Has one encounter
- Has one CPT code
- Has one number of units (integer for now, basically just a count)
- CPT Code
- Has one description
- No backrefs for now as there will be many, many line items
The API has the following methods:
Creates a patient.
JSON object body:
first_name
: stringlast_name
: string
JSON object:
id
: UUIDfirst_name
: stringlast_name
: string
Retrieves all patients.
None
JSON list of objects each containing the following:
id
: UUIDfirst_name
: stringlast_name
: string
Retrieves a patient.
In URL:
patient_id
: UUID of patient
JSON object:
id
: UUIDfirst_name
: stringlast_name
: string
Creates a patient encounter.
In URL:
patient_id
: UUID of patient
JSON object body:
date
: Date (YYYY-mm-dd format) of encounter
JSON object:
id
: Globally unique UUID of encounterdate
: Date of encounter
In URL:
patient_id
: UUID of patient
JSON list of objects each containing the following:
id
: Globally unique UUID of encounterdate
: Date of encounter
In URL:
patient_id
: UUID of patientencounter_id
: UUID of patient encounter
JSON object:
id
: Globally unique UUID of encounterdate
: Date of encounter
In URL:
patient_id
: UUID of patientencounter_id
: UUID of patient encounter
JSON object body:
cpt_code
: string, CPT code for line itemunits
: int, number of units for line item
JSON object:
cpt_code
: string, CPT code for line itemcpt_code_description
: string, CPT code descriptionunits
: int, number of units for line item
In URL:
patient_id
: UUID of patientencounter_id
: UUID of patient encounter
JSON list of objects each containing the following:
cpt_code
: string, CPT code for line itemcpt_code_description
: string, CPT code descriptionunits
: int, number of units for line item
- Peewee for the ORM because it's light.
- FastAPI for the REST API middleware because it's light too.
- Postgres for the database because it's easy to deploy for demo purposes.
- SQLite for unit testing because it doesn't need deployment.
- Kubernetes for deployment because who doesn't like making their local machine go brrrrrr.
- Tables are created and CPT codes are loaded at API start as part of an internal database migration step.
Some bad paths are not tested just for the purposes of a demo, but bad inputs do have some coverage.
---------- coverage: platform win32, python 3.11.9-final-0 -----------
Name Stmts Miss Cover
---------------------------------------------------
conftest.py 8 0 100%
src\api\api.py 14 4 71%
src\api\config.py 5 0 100%
src\api\db_config.py 13 4 69%
src\api\db_migrate.py 21 0 100%
src\api\model.py 29 0 100%
src\api\routers\__init__.py 6 0 100%
src\api\routers\api_input.py 9 0 100%
src\api\routers\api_output.py 22 0 100%
src\api\routers\encounters.py 54 19 65%
src\api\routers\health.py 7 1 86%
src\api\routers\line_items.py 46 4 91%
src\api\routers\patients.py 32 5 84%
src\api\routers\routers.py 3 0 100%
---------------------------------------------------
TOTAL 269 37 86%
Health checks spam the logs a bit.