|
| 1 | +# Tutorial: Adding a new API endpoint |
| 2 | + |
| 3 | +**Prerequisite:** this guide assumes that you have read the |
| 4 | +[epidata development guide](epidata_development.md). |
| 5 | + |
| 6 | +In this tutorial we'll create a brand new endpoint for the Epidata API: |
| 7 | +`fluview_meta`. At a high level, we'll do the following steps: |
| 8 | + |
| 9 | +1. understand the data that we want to surface |
| 10 | +2. add the new endpoint to `api.php` |
| 11 | +3. add the new endpoint to the various client libraries |
| 12 | +4. write an integration test for the new endpoint |
| 13 | +5. update API documentation for the new endpoint |
| 14 | +6. run all unit and integration tests |
| 15 | + |
| 16 | +# setup |
| 17 | + |
| 18 | +Follow |
| 19 | +[the backend guide](https://github.com/cmu-delphi/operations/blob/master/docs/backend_development.md) |
| 20 | +and [the epidata guide](epidata_development.md) to install Docker and get your |
| 21 | +workspace ready for development. Before continuing, your workspace should look |
| 22 | +something like the following: |
| 23 | + |
| 24 | +```bash |
| 25 | +tree -L 3 . |
| 26 | +``` |
| 27 | + |
| 28 | +``` |
| 29 | +. |
| 30 | +└── repos |
| 31 | + ├── delphi |
| 32 | + │ ├── delphi-epidata |
| 33 | + │ ├── flu-contest |
| 34 | + │ ├── github-deploy-repo |
| 35 | + │ ├── nowcast |
| 36 | + │ ├── operations |
| 37 | + │ └── utils |
| 38 | + └── undefx |
| 39 | + ├── py3tester |
| 40 | + └── undef-analysis |
| 41 | +``` |
| 42 | + |
| 43 | +# the data |
| 44 | + |
| 45 | +Here's the requirement: we need to quickly surface the most recent "issue" |
| 46 | +(epiweek of publication) for the existing [`fluview` endpoint](api/fluview.md). |
| 47 | +This is already provided by the existing [`meta` endpoint](api/meta.md), |
| 48 | +however, it's _very_ slow, and it returns a bunch of unrelated data. The goal |
| 49 | +is extract the subset of metadata pertaining to `fluview` and return just that |
| 50 | +data through a new endpoint. |
| 51 | + |
| 52 | +Each row in the `fluview` table contains |
| 53 | +[a lot of data](../src/ddl/fluview.sql), but we're particularly interested in |
| 54 | +the following: |
| 55 | + |
| 56 | +- latest publication date |
| 57 | +- latest "issue", which is the publication epiweek |
| 58 | +- total size of the table |
| 59 | + |
| 60 | +# update the server |
| 61 | + |
| 62 | +Open [`api.php`](../src/server/api.php) and navigate to the bottom where we see |
| 63 | +line like `if($source === 'NAME') { ... }`. Right below the `if` block for |
| 64 | +`if($source === 'fluview')`, add a new `if else` block for our new endpoint: |
| 65 | + |
| 66 | +```php |
| 67 | +else if($source === 'fluview_meta') { |
| 68 | + // get the data |
| 69 | + $epidata = meta_fluview(); |
| 70 | + store_result($data, $epidata); |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +Fortunately, the function `meta_fluview()` is already defined, so we can just |
| 75 | +reuse it. (It's used by the `meta` endpoint as mentioned above.) In general, |
| 76 | +you will likely need to define a new function named like |
| 77 | +`get_SOURCE(params...)`, especially if you're reading from a new database |
| 78 | +table. |
| 79 | + |
| 80 | +# update the client libraries |
| 81 | + |
| 82 | +There are currently four client libraries. They all need to be updated to make |
| 83 | +the new `fluview_meta` endpoint available to callers. The pattern is very |
| 84 | +similar for all endpoints, so copy-paste will get you 90% of the way there. |
| 85 | + |
| 86 | +`fluview_meta` is especially simple as it takes no parameters, and consequently |
| 87 | +there is no need to validate parameters. In general, it's a good idea to do |
| 88 | +sanity checks on caller inputs prior to sending the request to the API. See |
| 89 | +some of the other endpoint implementations (e.g. `fluview`) for an example of |
| 90 | +what this looks like. |
| 91 | + |
| 92 | +Here's what we add to each client: |
| 93 | + |
| 94 | +- [`delphi_epidata.coffee`](../src/client/delphi_epidata.coffee) |
| 95 | + |
| 96 | + ```coffeescript |
| 97 | + # Fetch FluView metadata |
| 98 | + @fluview_meta: (callback) -> |
| 99 | + # Set up request |
| 100 | + params = |
| 101 | + 'source': 'fluview_meta' |
| 102 | + # Make the API call |
| 103 | + _request(callback, params) |
| 104 | + ``` |
| 105 | + |
| 106 | +- [`delphi_epidata.js`](../src/client/delphi_epidata.js) |
| 107 | + |
| 108 | + Note that this file _can and should be generated from |
| 109 | + `delphi_epidata.coffee`_. However, for trivial changes, like the addition |
| 110 | + of this very simple endpoint, it may be slightly faster, _though |
| 111 | + error-prone_, to just update the JavaScript manually. |
| 112 | + |
| 113 | + ```javascript |
| 114 | + Epidata.fluview_meta = function(callback) { |
| 115 | + var params; |
| 116 | + params = { |
| 117 | + 'source': 'fluview_meta' |
| 118 | + }; |
| 119 | + return _request(callback, params); |
| 120 | + }; |
| 121 | + ``` |
| 122 | + |
| 123 | +- [`delphi_epidata.py`](../src/client/delphi_epidata.py) |
| 124 | + |
| 125 | + Note that this file, unlike the others, is released as a public package, |
| 126 | + available to install easily though Python's `pip` tool. That package should |
| 127 | + be updated once the code is committed, however that is outside of the scope |
| 128 | + of this tutorial. |
| 129 | +
|
| 130 | + ```python |
| 131 | + # Fetch FluView metadata |
| 132 | + @staticmethod |
| 133 | + def fluview_meta(): |
| 134 | + """Fetch FluView metadata.""" |
| 135 | + # Set up request |
| 136 | + params = { |
| 137 | + 'source': 'fluview_meta', |
| 138 | + } |
| 139 | + # Make the API call |
| 140 | + return Epidata._request(params) |
| 141 | + ``` |
| 142 | +
|
| 143 | +- [`delphi_epidata.R`](../src/client/delphi_epidata.R) |
| 144 | +
|
| 145 | + ```R |
| 146 | + # Fetch FluView metadata |
| 147 | + fluview_meta <- function() { |
| 148 | + # Set up request |
| 149 | + params <- list( |
| 150 | + source = 'fluview_meta' |
| 151 | + ) |
| 152 | + # Make the API call |
| 153 | + return(.request(params)) |
| 154 | + } |
| 155 | + ``` |
| 156 | +
|
| 157 | + **This file requires a second change: updating the list of exported |
| 158 | + functions.** This additional step only applies to this particular client |
| 159 | + library. At the bottom of the file, inside of `return(list(`, add the |
| 160 | + following line to make the function available to callers. |
| 161 | +
|
| 162 | + ```R |
| 163 | + fluview_meta = fluview_meta, |
| 164 | + ``` |
| 165 | +
|
| 166 | +# add an integration test |
| 167 | +
|
| 168 | +Now that we've changed several files, we need to make sure that the changes |
| 169 | +work as intended _before_ submitting code for review or committing code to the |
| 170 | +repository. Given that the code spans multiple components and languages, this |
| 171 | +needs to be an integration test. See more about integration testing in Delphi's |
| 172 | +[frontend development guide](https://github.com/cmu-delphi/operations/blob/master/docs/frontend_development.md#integration). |
| 173 | +
|
| 174 | +Create an integration test for the new endpoint by creating a new file |
| 175 | +`integrations/server/test_fluview_meta.py`. There's a good amount of |
| 176 | +boilerplate, but fortunately this can be copied _almost_ verbatim from the |
| 177 | +[`fluview` endpoint integration test](../integrations/server/test_fluview.py). |
| 178 | + |
| 179 | +Include the following pieces: |
| 180 | + |
| 181 | +- top-level docstring (update name to `fluview_meta`) |
| 182 | +- the imports section (no changes needed) |
| 183 | +- the test class (update name and docstring for `fluview_meta`) |
| 184 | +- the methods `setUpClass`, `setUp`, and `tearDown` (no changes needed) |
| 185 | + |
| 186 | +Add the following test method which creates some dummy data, fetches the new |
| 187 | +`fluview_meta` endpoint using the Python client library, and asserts that the |
| 188 | +returned value is what we expect. |
| 189 | + |
| 190 | +```python |
| 191 | +def test_round_trip(self): |
| 192 | + """Make a simple round-trip with some sample data.""" |
| 193 | + |
| 194 | + # insert dummy data |
| 195 | + self.cur.execute(''' |
| 196 | + insert into fluview values |
| 197 | + (0, "2020-04-07", 202021, 202020, "nat", 1, 2, 3, 4, 3.14159, 1.41421, |
| 198 | + 10, 11, 12, 13, 14, 15), |
| 199 | + (0, "2020-04-28", 202022, 202022, "hhs1", 5, 6, 7, 8, 1.11111, 2.22222, |
| 200 | + 20, 21, 22, 23, 24, 25) |
| 201 | + ''') |
| 202 | + self.cnx.commit() |
| 203 | + |
| 204 | + # make the request |
| 205 | + response = Epidata.fluview_meta() |
| 206 | + |
| 207 | + # assert that the right data came back |
| 208 | + self.assertEqual(response, { |
| 209 | + 'result': 1, |
| 210 | + 'epidata': [{ |
| 211 | + 'latest_update': '2020-04-28', |
| 212 | + 'latest_issue': 202022, |
| 213 | + 'table_rows': 2, |
| 214 | + }], |
| 215 | + 'message': 'success', |
| 216 | + }) |
| 217 | +``` |
| 218 | + |
| 219 | +# write documentation |
| 220 | + |
| 221 | +This consists of two steps: add a new document for the `fluview_meta` endpoint, |
| 222 | +and add a new entry to the existing table of endpoints. |
| 223 | + |
| 224 | +Create a new file `docs/api/fluview_meta.md`. Copy as much as needed from other |
| 225 | +endpoints, e.g. [the fluview documentation](api/fluview.md). Update the |
| 226 | +description, table of return values, and sample code and URLs as needed. |
| 227 | + |
| 228 | +Edit the table of endpoints in [`docs/api/README.md`](api/README.md), adding |
| 229 | +the following row in the appropriate place (i.e. next to the row for |
| 230 | +`fluview`): |
| 231 | + |
| 232 | +``` |
| 233 | +| [`fluview_meta`](fluview_meta.md) | FluView Metadata | Summary data about [`fluview`](fluview.md). | no | |
| 234 | +``` |
| 235 | + |
| 236 | +# run tests |
| 237 | + |
| 238 | +## unit |
| 239 | + |
| 240 | +Finally, we just need to run all new and existing tests. It is recommended to |
| 241 | +start with the unit tests because they are faster to build, run, and either |
| 242 | +succeed or fail. Follow the |
| 243 | +[backend development guide](https://github.com/cmu-delphi/operations/blob/master/docs/backend_development.md#running-a-container). |
| 244 | +In summary: |
| 245 | + |
| 246 | +```bash |
| 247 | +# build the image |
| 248 | +docker build -t delphi_python \ |
| 249 | + -f repos/delphi/operations/dev/docker/python/Dockerfile . |
| 250 | + |
| 251 | +# run epidata unit tests |
| 252 | +docker run --rm delphi_python \ |
| 253 | + python3 -m undefx.py3tester.py3tester --color \ |
| 254 | + repos/delphi/delphi-epidata/tests |
| 255 | +``` |
| 256 | + |
| 257 | +If all succeeds, output should look like this: |
| 258 | + |
| 259 | +``` |
| 260 | +[...] |
| 261 | +
|
| 262 | +✔ All 48 tests passed! 69% (486/704) coverage. |
| 263 | +``` |
| 264 | + |
| 265 | +## integration |
| 266 | + |
| 267 | +Integration tests require more effort, and take longer to setup and run. |
| 268 | +However, they allow us to test that various pieces are working together |
| 269 | +correctly. Many of these pieces we can't test individually with unit tests |
| 270 | +(e.g. database, and the API server), so integration tests are the only way we |
| 271 | +can be confident that our changes won't break the API. Follow the [epidata |
| 272 | +development guide](epidata_development.md#test). In summary, assuming you have |
| 273 | +already built the `delphi_python` image above: |
| 274 | + |
| 275 | +```bash |
| 276 | +# build web and database images for epidata |
| 277 | +docker build -t delphi_web \ |
| 278 | + -f repos/delphi/operations/dev/docker/web/Dockerfile . |
| 279 | +docker build -t delphi_web_epidata \ |
| 280 | + -f repos/delphi/delphi-epidata/dev/docker/web/epidata/Dockerfile . |
| 281 | +docker build -t delphi_database \ |
| 282 | + -f repos/delphi/operations/dev/docker/database/Dockerfile . |
| 283 | +docker build -t delphi_database_epidata \ |
| 284 | + -f repos/delphi/delphi-epidata/dev/docker/database/epidata/Dockerfile . |
| 285 | + |
| 286 | +# launch web and database containers in separate terminals |
| 287 | +docker run --rm -p 13306:3306 \ |
| 288 | + --network delphi-net --name delphi_database_epidata \ |
| 289 | + delphi_database_epidata |
| 290 | + |
| 291 | +docker run --rm -p 10080:80 \ |
| 292 | + --network delphi-net --name delphi_web_epidata \ |
| 293 | + delphi_web_epidata |
| 294 | + |
| 295 | +# wait for the above containers to initialize (~15 seconds) |
| 296 | + |
| 297 | +# run integration tests |
| 298 | +docker run --rm --network delphi-net delphi_python \ |
| 299 | + python3 -m undefx.py3tester.py3tester --color \ |
| 300 | + repos/delphi/delphi-epidata/integrations |
| 301 | +``` |
| 302 | + |
| 303 | +If all succeeds, output should look like this. Note also that our new |
| 304 | +integration test specifically passed. |
| 305 | + |
| 306 | +``` |
| 307 | +[...] |
| 308 | +
|
| 309 | +delphi.delphi-epidata.integrations.server.test_fluview_meta.FluviewMetaTests.test_round_trip: pass |
| 310 | +
|
| 311 | +[...] |
| 312 | +
|
| 313 | +✔ All 16 tests passed! 48% (180/372) coverage. |
| 314 | +``` |
| 315 | + |
| 316 | +# code review and submission |
| 317 | + |
| 318 | +All tests pass, and the changes are working as intended. Now submit the code |
| 319 | +for review, e.g. by opening a pull request on GitHub. For an example, see the |
| 320 | +actual |
| 321 | +[pull request for the `fluview_meta` endpoint](https://github.com/cmu-delphi/delphi-epidata/pull/93) |
| 322 | +created in this tutorial. |
| 323 | + |
| 324 | +Once it's approved, commit the code. Within a short amount of time (usually ~30 |
| 325 | +seconds), the API will begin serving your new endpoint. Go ahead and give it a |
| 326 | +try: https://delphi.midas.cs.cmu.edu/epidata/api.php?source=fluview_meta |
| 327 | + |
| 328 | +``` |
| 329 | +{ |
| 330 | + "result": 1, |
| 331 | + "epidata": [ |
| 332 | + { |
| 333 | + "latest_update": "2020-04-24", |
| 334 | + "latest_issue": 202016, |
| 335 | + "table_rows": 957673 |
| 336 | + } |
| 337 | + ], |
| 338 | + "message": "success" |
| 339 | +} |
| 340 | +``` |
0 commit comments