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

jidea-56 Metadata endpoint #5

Merged
merged 13 commits into from
Aug 9, 2024
Merged

jidea-56 Metadata endpoint #5

merged 13 commits into from
Aug 9, 2024

Conversation

EmmaLRussell
Copy link
Contributor

@EmmaLRussell EmmaLRussell commented Aug 6, 2024

This branch adds the first cut of a metadata endpoint to allow the web app to get going with parameters metadata.

I was getting a bit bogged down with attempting to implement the versioning part of this, so have spun that out into a separate ticket (jidea-62) - but I've included a description of the intended approach in the README here. For now, we always read in a hardcoded metadata version (0.1.0), and this version is also returned in the response.

This branch implements:

  • metadata schema
  • /metadata endpoint which:
  • reads metadata from json file
  • injects country names from daedalus package - this will eventually include ISO ids from daedalus too, but for now country ids are just the names
  • injects hardcoded pathogen options - these will eventually come from daedalus too

I am very bad at R - please let me know anything that could have been done more sensibly in a different way!

To see the endpoint in action:

docker pull mrcide/daedalus.api:jidea-56
docker run --rm -d -p 8001:8001 --name dapi mrcide/daedalus.api:jidea-56
curl -s http://localhost:8001/metadata | jq

then docker kill dapi to tear down

Comment on lines +9 to +16
system_file <- function(...) {
tryCatch({
system.file(..., mustWork = TRUE, package = "daedalus.api")
}, error = function(e) {
stop(sprintf("Failed to locate file from args\n%s",
paste(list(...), collapse = " ")))
})
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handy method stolen from hintr!

@EmmaLRussell EmmaLRussell marked this pull request as ready for review August 8, 2024 11:03
Copy link
Contributor

@david-mears-2 david-mears-2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 comment and 1 question. Looks good as far as I can tell without my glaRses on!

"defaultOption": "none",
"ordered": true,
"options": [
{ "id": "none", "label": "None" },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more of a question than a comment, but it's possible in the future we'll need to annotate each such option with a description to explain what precisely is meant by e.g. 'Medium'. I presume it's OK to cross that bridge when we get to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we can add a "description" field if required.

R/parameters.R Outdated
Comment on lines 1 to 7
# Allowed parameter ids which we can pass to the model
expected_parameters <- c(
"country",
"pathogen",
"response",
"vaccine"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a bit misleading to store data that is used only by a test, outside of the tests folder.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, true - but I think we might expand on and make further user of this. For example, we might want to validate the parameters in run requests... (although maybe we just let the package do that?) Happy to move it into tests for now though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll let @pratikunterwegs opine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could live in tests for now and move to internal package data once the need becomes clear.

Copy link
Contributor

@pratikunterwegs pratikunterwegs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @EmmaLRussell, looks good to me and I was able to follow the Readme instructions to pull and run the container. Just a few small suggestions here and there.

R/util.R Outdated
Comment on lines 18 to 21
read_json <- function(filename) {
json <- jsonlite::fromJSON(system_file("json", filename))
json
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be replaced by the {jsonlite} function of the same name, with simplifyVector = TRUE? Or alternatively, using jsonlite::fromJSON() directly? The wrapper being the same name as the {jsonlite} had me confused what it was doing (especially as the behaviour, e.g. simplifyVector is the opposite of what the {jsonlite} version does).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point. How about if I change the name to something not shared with jsonlite to make it clear this is a special for the API? read_local_json_file? I'm just thinking that neither fromJSON nor read_json are going to do exactly the same as their jsonlite equivalent since it's selecting the folder to read from.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read_local_json() sounds good!

Comment on lines +9 to +16
system_file <- function(...) {
tryCatch({
system.file(..., mustWork = TRUE, package = "daedalus.api")
}, error = function(e) {
stop(sprintf("Failed to locate file from args\n%s",
paste(list(...), collapse = " ")))
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why this function has been included, although it's over-writing a well known base function; the informative error is useful. Could we get a small comment explaining that?

R/api.R Outdated
# JIDEA-62: we will read in correct metadata version according to
# model_version
metadata_file <- sprintf("metadata_%s.json", model_version)
response <- read_json(metadata_file)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was initially confused as to where this function was coming from, but now I see it's redefined in this package; I've added a suggestion to switch to jsonlite::fromJSON() instead

R/api.R Outdated
metadata <- function() {
# JIDEA-62: we will use relevant model version - from qs if specified, else
# from latest available metadata file
model_version <- scalar("0.1.0")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this pull from the package version for now? The model structure and data versions are all bundled together in the package version at the moment anyway.


test_that("Can get metadata", {
endpoint <- daedalus_api_endpoint("GET", "/metadata")
res <- endpoint$run()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could I suggest a no conditions expectation here:

Suggested change
res <- endpoint$run()
expect_no_condition(
res <- endpoint$run()
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, and I've put it on the root call too..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, that upsets the linter though: Avoid implicit assignments in function calls.
So have excluded those lines from linting!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, a classic. The other linter complaints can usually be fixed with a styler::style_pkg() call

Comment on lines 33 to 35
lapply(res$data$parameters$id, function(id) {
expect_true(id %in% expected_parameters)
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be simplified to an expect_identical(); if that proves too strict later it can always be changed to expect_setequal() or similar.

Suggested change
lapply(res$data$parameters$id, function(id) {
expect_true(id %in% expected_parameters)
})
expect_identical(
res$data$parameters$id,
expected_parameters
)

Comment on lines 41 to 45
lapply(seq_along(country_options), function(idx) {
daedalus_country <- scalar(daedalus_countries[[idx]])
expect_identical(country_options[[idx]]$id, daedalus_country)
expect_identical(country_options[[idx]]$label, daedalus_country)
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could I suggest sort of transposing this chunk so the outermost function always returns the expectations - no sweat really as this does what's needed. If you keep the original version, I don't think the scalar() call on daedalus_countries is needed.

Suggested change
lapply(seq_along(country_options), function(idx) {
daedalus_country <- scalar(daedalus_countries[[idx]])
expect_identical(country_options[[idx]]$id, daedalus_country)
expect_identical(country_options[[idx]]$label, daedalus_country)
})
expect_identical(
vapply(country_options, `[[`, FUN.VALUE = character(1), "id"),
daedalus_countries
)
expect_identical(
vapply(country_options, `[[`, FUN.VALUE = character(1), "label"),
daedalus_countries
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone for a version of this approach, but with syntax that I find easier to cope with! (more verbose though..)

# expect country ids to match those from daedalus
  expect_identical(
    vapply(country_options, function(option) {
        option$id
    }, character(1L)),
    daedalus_countries
  )
  # expect country labels to match those from daedalus
  expect_identical(
    vapply(country_options, function(option) {
      option$label
    }, character(1L)),
    daedalus_countries
  )

@david-mears-2
Copy link
Contributor

Thank you, it’s awesome to have this in place!

Copy link
Contributor

@pratikunterwegs pratikunterwegs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @EmmaLRussell looks good!

@EmmaLRussell
Copy link
Contributor Author

Thanks both!

@EmmaLRussell EmmaLRussell merged commit b531d80 into main Aug 9, 2024
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants