From 4d09603c9288ae15862649c9d961ddac43d4f664 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Tue, 24 Sep 2024 16:07:48 +0200 Subject: [PATCH 01/27] Started R package using usethis library --- R/remotebmi/.Rbuildignore | 1 + R/remotebmi/DESCRIPTION | 21 +++ R/remotebmi/LICENSE.md | 194 +++++++++++++++++++++++ R/remotebmi/NAMESPACE | 2 + R/remotebmi/R/server.R | 43 +++++ R/remotebmi/README.md | 31 ++++ R/remotebmi/tests/testthat.R | 12 ++ R/remotebmi/tests/testthat/test-server.R | 4 + 8 files changed, 308 insertions(+) create mode 100644 R/remotebmi/.Rbuildignore create mode 100644 R/remotebmi/DESCRIPTION create mode 100644 R/remotebmi/LICENSE.md create mode 100644 R/remotebmi/NAMESPACE create mode 100644 R/remotebmi/R/server.R create mode 100644 R/remotebmi/README.md create mode 100644 R/remotebmi/tests/testthat.R create mode 100644 R/remotebmi/tests/testthat/test-server.R diff --git a/R/remotebmi/.Rbuildignore b/R/remotebmi/.Rbuildignore new file mode 100644 index 0000000..5163d0b --- /dev/null +++ b/R/remotebmi/.Rbuildignore @@ -0,0 +1 @@ +^LICENSE\.md$ diff --git a/R/remotebmi/DESCRIPTION b/R/remotebmi/DESCRIPTION new file mode 100644 index 0000000..070b3ae --- /dev/null +++ b/R/remotebmi/DESCRIPTION @@ -0,0 +1,21 @@ +Package: remotebmi +Title: BMI over http +Version: 0.0.0.9000 +Authors@R: + person("Stefan", "Verhoeven", , "s.verhoeven@esciencecenter.nl", role = c("aut", "cre"), + comment = c(ORCID = "https://orcid.org/0000-0002-5821-2060")) +Description: Run a R BMI model as a http json web service +URL: https://github.com/eWaterCycle/remotebmi +License: Apache License (>= 2) +Encoding: UTF-8 +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.3.2 +Imports: + fiery, + reqres, + routr +Suggests: + testthat (>= 3.0.0) +Config/testthat/edition: 3 +Remotes: + eWaterCycle/bmi-r diff --git a/R/remotebmi/LICENSE.md b/R/remotebmi/LICENSE.md new file mode 100644 index 0000000..b62a9b5 --- /dev/null +++ b/R/remotebmi/LICENSE.md @@ -0,0 +1,194 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/R/remotebmi/NAMESPACE b/R/remotebmi/NAMESPACE new file mode 100644 index 0000000..6ae9268 --- /dev/null +++ b/R/remotebmi/NAMESPACE @@ -0,0 +1,2 @@ +# Generated by roxygen2: do not edit by hand + diff --git a/R/remotebmi/R/server.R b/R/remotebmi/R/server.R new file mode 100644 index 0000000..39d68a5 --- /dev/null +++ b/R/remotebmi/R/server.R @@ -0,0 +1,43 @@ +library(fiery) +library(routr) +library(reqres) + +# TODO move routes to separate file +bmi_initialize <- function(request, response, keys, ...) { + request$parse(json = parse_json()) + model$bmi_initialize(request$body$config_file) + response$status <- 201L + return(FALSE) +} + +get_component_name <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- list(name = model$get_component_name()) + response$format(json = format_json()) + return(FALSE) +} + +get_output_var_names <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$get_output_var_names() + response$format(json = format_json()) + return(FALSE) +} + +serve <- function(port = 50051) { + route <- Route$new() + route$add_handler('get', '/get_component_name', get_component_name) + route$add_handler('get', '/get_output_var_names', get_output_var_names) + route$add_handler('post', '/initialize', bmi_initialize) + + router <- RouteStack$new() + router$add_route(route, 'bmi') + + port = as.integer(Sys.getenv("BMI_PORT", port)) + # TODO set-able host + app <- Fire$new(port=port) + app$attach(router) + app$ignite() +} \ No newline at end of file diff --git a/R/remotebmi/README.md b/R/remotebmi/README.md new file mode 100644 index 0000000..428288c --- /dev/null +++ b/R/remotebmi/README.md @@ -0,0 +1,31 @@ + +# remotebmi + + + + +The goal of remotebmi is to ... + +## Installation + +You can install the development version of remotebmi from [GitHub](https://github.com/) with: + +``` r +# install.packages("pak") +pak::pak("eWaterCycle/remotebmi") +``` + +## Example + +This is a basic example which shows you how to solve a common problem: + +``` r +library(remotebmi) +## basic example code + +source('https://github.com/eWaterCycle/grpc4bmi-examples/raw/master/walrus/walrus-bmi.r') +model <- WalrusBmi$new() + +remotebmi::serve() +``` + diff --git a/R/remotebmi/tests/testthat.R b/R/remotebmi/tests/testthat.R new file mode 100644 index 0000000..4cdde84 --- /dev/null +++ b/R/remotebmi/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(remotebmi) + +test_check("remotebmi") diff --git a/R/remotebmi/tests/testthat/test-server.R b/R/remotebmi/tests/testthat/test-server.R new file mode 100644 index 0000000..481e1da --- /dev/null +++ b/R/remotebmi/tests/testthat/test-server.R @@ -0,0 +1,4 @@ +# TODO test routes +test_that("multiplication works", { + expect_equal(2 * 2, 4) +}) From 27c1c8a9ff1d0c275ea9a01f78f94f5ef55e2b6c Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 25 Sep 2024 13:36:44 +0200 Subject: [PATCH 02/27] Implement remotebmi server in R --- .gitignore | 4 +- R/remotebmi/DESCRIPTION | 2 +- R/remotebmi/NAMESPACE | 1 + R/remotebmi/R/route.R | 344 +++++++++++++++++++++++ R/remotebmi/R/server.R | 52 ++-- R/remotebmi/README.md | 53 +++- R/remotebmi/man/serve.Rd | 23 ++ R/remotebmi/tests/testthat/test-route.R | 45 +++ R/remotebmi/tests/testthat/test-server.R | 4 - 9 files changed, 485 insertions(+), 43 deletions(-) create mode 100644 R/remotebmi/R/route.R create mode 100644 R/remotebmi/man/serve.Rd create mode 100644 R/remotebmi/tests/testthat/test-route.R delete mode 100644 R/remotebmi/tests/testthat/test-server.R diff --git a/.gitignore b/.gitignore index 5079cc5..f7f5686 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,6 @@ python/heat.toml RemoteBMI.jl/example/Project.toml RemoteBMI.jl/example/heat.toml openapi-generator-cli.jar -openapitools.json \ No newline at end of file +openapitools.json +PEQ_Hupsel.dat +walrus.yml diff --git a/R/remotebmi/DESCRIPTION b/R/remotebmi/DESCRIPTION index 070b3ae..7b0f550 100644 --- a/R/remotebmi/DESCRIPTION +++ b/R/remotebmi/DESCRIPTION @@ -4,7 +4,7 @@ Version: 0.0.0.9000 Authors@R: person("Stefan", "Verhoeven", , "s.verhoeven@esciencecenter.nl", role = c("aut", "cre"), comment = c(ORCID = "https://orcid.org/0000-0002-5821-2060")) -Description: Run a R BMI model as a http json web service +Description: Runs a BMI model as a http json web service. URL: https://github.com/eWaterCycle/remotebmi License: Apache License (>= 2) Encoding: UTF-8 diff --git a/R/remotebmi/NAMESPACE b/R/remotebmi/NAMESPACE index 6ae9268..66ce286 100644 --- a/R/remotebmi/NAMESPACE +++ b/R/remotebmi/NAMESPACE @@ -1,2 +1,3 @@ # Generated by roxygen2: do not edit by hand +export(serve) diff --git a/R/remotebmi/R/route.R b/R/remotebmi/R/route.R new file mode 100644 index 0000000..fd31a8e --- /dev/null +++ b/R/remotebmi/R/route.R @@ -0,0 +1,344 @@ + + +last_segment <- function(path) { + # keys get toLower at https://github.com/thomasp85/routr/blob/8605611a10607016a83660f83f310075787a27b2/R/route.R#L250 + # need untouched version + segments <- unlist(strsplit(path, '/')) + return(segments[length(segments)]) +} + + +#' Create a Route for the Given Model +#' +#' This function generates a route for the specified model. The route is used to +#' facilitate communication and interaction with the model. +#' +#' @param model The model instance to be used in route handlers Must implement the subclass of [AbstractBmi](https://github.com/eWaterCycle/bmi-r/blob/master/R/abstract-bmi.R) +#' +#' @return A route object that can be used to interact with the model. +#' +#' @examples +#' \dontrun{ +#' model <- SomeModel$new() +#' route <- create_route(model) +#' } +#' +#' @export +create_route <- function(model) { + bmi_initialize <- function(request, response, keys, ...) { + request$parse(json = reqres::parse_json()) + model$bmi_initialize(request$body$config_file) + response$status <- 201L + return(FALSE) + } + + update <- function(request, response, keys, ...) { + model$update() + response$status <- 204L + return(FALSE) + } + + update_until <- function(request, response, keys, ...) { + request$parse(json = reqres::parse_json()) + until = request$body + model$updateUntil(until) + response$status <- 204L + return(FALSE) + } + + finalize <- function(request, response, keys, ...) { + model$bmi_finalize() + response$status <- 204L + return(FALSE) + } + + get_component_name <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- list(name = model$getComponentName()) + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_output_var_names <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getOutputVarNames() + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_input_var_names <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getInputVarNames() + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_time_units <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- list(units = model$getTimeUnits()) + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_time_step <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getTimeStep() + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_current_time <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getCurrentTime() + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_start_time <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getStartTime() + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_end_time <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getEndTime() + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_var_grid <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getVarGrid(last_segment(request$path)) + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_var_type <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + rawType <- model$getVarType(last_segment(request$path)) + type <- ifelse(rawType == 'float64', 'double', rawType) + # TODO map other types to double, float, int32 or int64 + response$body <- list(type = type) + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_var_itemsize <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getVarItemSize(last_segment(request$path)) + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_var_units <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- list(units = model$getVarUnits(last_segment(request$path))) + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_var_nbytes <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getVarNBytes(last_segment(request$path)) + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_value <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getValue(last_segment(request$path)) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_value_at_indices <- function(request, response, keys, ...) { + request$parse(json = reqres::parse_json()) + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getValueAtIndices(last_segment(request$path), request$body) + response$format(json = reqres::format_json()) + return(FALSE) + } + + set_value <- function(request, response, keys, ...) { + request$parse(json = reqres::parse_json()) + model$setValue(last_segment(request$path), request$body) + response$status <- 204L + return(FALSE) + } + + set_value_at_indices <- function(request, response, keys, ...) { + request$parse(json = reqres::parse_json()) + model$setValueAtIndices(last_segment(request$path), request$body$indices, request$body$values) + response$status <- 204L + return(FALSE) + } + + get_grid_rank <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getGridRank(keys$grid) + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_grid_type <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- list(type = model$getGridType(keys$grid)) + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_grid_size <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getGridSize(keys$grid) + response$format(json = reqres::format_json(auto_unbox=TRUE)) + return(FALSE) + } + + get_grid_x <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getGridX(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_y <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getGridY(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_z <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getGridZ(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_origin <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getGridOrigin(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_shape <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getGridShape(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_spacing <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getGridSpacing(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_connectivity <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getGridConnectivity(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_offset <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- 'application/json' + response$body <- model$getGridOffset(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + route <- routr::Route$new() + # IRF + route$add_handler('post', '/initialize', bmi_initialize) + route$add_handler('post', '/update', update) + route$add_handler('post', '/update_until', update_until) + route$add_handler('delete', '/finalize', finalize) + + # Exchange items + route$add_handler('get', '/get_component_name', get_component_name) + route$add_handler('get', '/get_output_var_names', get_output_var_names) + route$add_handler('get', '/get_input_var_names', get_input_var_names) + + # Getters + route$add_handler('get', '/get_value/:name', get_value) + route$add_handler('get', '/get_value_at_indices/:name', get_value_at_indices) + + # Setters + route$add_handler('post', '/set_value/:name', set_value) + route$add_handler('post', '/set_value_at_indices/:name', set_value_at_indices) + + # Time information + route$add_handler('get', '/get_time_units', get_time_units) + route$add_handler('get', '/get_time_step', get_time_step) + route$add_handler('get', '/get_current_time', get_current_time) + route$add_handler('get', '/get_start_time', get_start_time) + route$add_handler('get', '/get_end_time', get_end_time) + + # Variable information + route$add_handler('get', '/get_var_grid/:name', get_var_grid) + route$add_handler('get', '/get_var_type/:name', get_var_type) + route$add_handler('get', '/get_var_itemsize/:name', get_var_itemsize) + route$add_handler('get', '/get_var_units/:name', get_var_units) + route$add_handler('get', '/get_var_nbytes/:name', get_var_nbytes) + + # Grid information + route$add_handler('get', '/get_grid_rank/:grid', get_grid_rank) + route$add_handler('get', '/get_grid_type/:grid', get_grid_type) + route$add_handler('get', '/get_grid_size/:grid', get_grid_size) + + # NURC + route$add_handler('get', '/get_grid_x/:grid', get_grid_x) + route$add_handler('get', '/get_grid_y/:grid', get_grid_y) + route$add_handler('get', '/get_grid_z/:grid', get_grid_z) + + # Uniform rectilinear + route$add_handler('get', '/get_grid_origin/:grid', get_grid_origin) + route$add_handler('get', '/get_grid_shape/:grid', get_grid_shape) + route$add_handler('get', '/get_grid_spacing/:grid', get_grid_spacing) + + # Unstructured + route$add_handler('get', '/get_grid_connectivity/:grid', get_grid_connectivity) + route$add_handler('get', '/get_grid_offset/:grid', get_grid_offset) + + # TODO Needed? + hFallback <- function(request, response, keys, ...) { + response$status <- 404L + response$type <- 'text/plain' + response$body <- 'Not found' + return(FALSE) + } + route$add_handler('get', '/*', hFallback) + + return(route) +} \ No newline at end of file diff --git a/R/remotebmi/R/server.R b/R/remotebmi/R/server.R index 39d68a5..aa50a08 100644 --- a/R/remotebmi/R/server.R +++ b/R/remotebmi/R/server.R @@ -2,42 +2,26 @@ library(fiery) library(routr) library(reqres) -# TODO move routes to separate file -bmi_initialize <- function(request, response, keys, ...) { - request$parse(json = parse_json()) - model$bmi_initialize(request$body$config_file) - response$status <- 201L - return(FALSE) -} - -get_component_name <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- list(name = model$get_component_name()) - response$format(json = format_json()) - return(FALSE) -} - -get_output_var_names <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$get_output_var_names() - response$format(json = format_json()) - return(FALSE) -} - -serve <- function(port = 50051) { - route <- Route$new() - route$add_handler('get', '/get_component_name', get_component_name) - route$add_handler('get', '/get_output_var_names', get_output_var_names) - route$add_handler('post', '/initialize', bmi_initialize) - - router <- RouteStack$new() +#' Serve the BMI model +#' +#' This function serves the model on a specified port and host. +#' +#' @param model The model instance to be served. Must implement the subclass of [AbstractBmi](https://github.com/eWaterCycle/bmi-r/blob/master/R/abstract-bmi.R) +#' @param port The port to serve the model on. Default is 50051 or if BMI_PORT environment variable is set, it will be used. +#' @param host The host to serve the model on. Default is "127.0.0.1". +#' @param ignite Whether to ignite the server immediately and block. Default is TRUE. +#' @return The server application. +#' @export +serve <- function(model, port = 50051, host = "127.0.0.1", ignite = TRUE) { + route = create_route(model) + router <- routr::RouteStack$new() router$add_route(route, 'bmi') port = as.integer(Sys.getenv("BMI_PORT", port)) - # TODO set-able host - app <- Fire$new(port=port) + app <- fiery::Fire$new(host=host, port=port) app$attach(router) - app$ignite() + if (ignite) { + app$ignite() + } + return(app) } \ No newline at end of file diff --git a/R/remotebmi/README.md b/R/remotebmi/README.md index 428288c..9710f76 100644 --- a/R/remotebmi/README.md +++ b/R/remotebmi/README.md @@ -4,7 +4,7 @@ -The goal of remotebmi is to ... +The goal of remotebmi is to allow a model with BMI interface written in R to be called from a Python client via a http json webservice. ## Installation @@ -12,7 +12,7 @@ You can install the development version of remotebmi from [GitHub](https://githu ``` r # install.packages("pak") -pak::pak("eWaterCycle/remotebmi") +pak::pak("github::eWaterCycle/remotebmi/R/remotebmi") ``` ## Example @@ -23,9 +23,56 @@ This is a basic example which shows you how to solve a common problem: library(remotebmi) ## basic example code +pak::pak("github::ClaudiaBrauer/WALRUS") +pak::pak("github::eWaterCycle/bmi-r") +pak::pak('configr') source('https://github.com/eWaterCycle/grpc4bmi-examples/raw/master/walrus/walrus-bmi.r') model <- WalrusBmi$new() -remotebmi::serve() +remotebmi::serve(model) +Fire started at 127.0.0.1:50051 ``` +With Python client test the model + +```python +import os +from remotebmi.client.client import RemoteBmiClient +from remotebmi.reserve import reserve_values +import numpy as np + +client = RemoteBmiClient('http://localhost:50051') +!wget https://github.com/eWaterCycle/grpc4bmi-examples/raw/refs/heads/master/walrus/walrus.yml +!wget https://github.com/ClaudiaBrauer/WALRUS/raw/refs/heads/master/demo/data/PEQ_Hupsel.dat +# Make data path in walrus.yml absolute +client.initialize( os.getcwd() + '/walrus.yml') +client.get_component_name() +'WALRUS' + +client.update() +client.get_current_time() +367417 +client.get_time_units() +'hours since 1970-01-01 00:00:00.0 00:00' +client.get_output_var_names() +['ETact', 'Q', 'fGS', 'fQS', 'dV', 'dVeq', 'dG', 'hQ', 'hS', 'w'] +# TODO add to AbstractBmi and WalrusBmi +# client.get_var_location('Q') +client.get_var_type('Q') +numpy.float64 +client.get_var_grid('Q') +0 +client.get_grid_type(0) +'rectilinear' +client.get_value('Q', dest=np.array([.1])) +array([0.0044]) +client.get_var_nbytes('Q') +'mm/h' +# TODO get_var_nbytes should return int not str +# this breaks reserve_values() aswell +dest = reserve_values(client, 'Q') +r = client.get_value('Q', dest) +r +client.finalize() + +``` \ No newline at end of file diff --git a/R/remotebmi/man/serve.Rd b/R/remotebmi/man/serve.Rd new file mode 100644 index 0000000..b4d9f58 --- /dev/null +++ b/R/remotebmi/man/serve.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/server.R +\name{serve} +\alias{serve} +\title{Serve the BMI model} +\usage{ +serve(model, port = 50051, host = "127.0.0.1", ignite = TRUE) +} +\arguments{ +\item{model}{The model to be served. Must implement the subclass of \href{https://github.com/eWaterCycle/bmi-r/blob/master/R/abstract-bmi.R}{AbstractBmi}} + +\item{port}{The port to serve the model on. Default is 50051.} + +\item{host}{The host to serve the model on. Default is "127.0.0.1".} + +\item{ignite}{Whether to ignite the server immediately and block. Default is TRUE.} +} +\value{ +The server application. +} +\description{ +This function serves the model on a specified port and host. +} diff --git a/R/remotebmi/tests/testthat/test-route.R b/R/remotebmi/tests/testthat/test-route.R new file mode 100644 index 0000000..93b986e --- /dev/null +++ b/R/remotebmi/tests/testthat/test-route.R @@ -0,0 +1,45 @@ + +# Poor mans mock +bmi_initialize_called_with <<- '' +# Mock model object +mock_model <- list( + bmi_initialize = function(config_file) { + bmi_initialize_called_with <<- config_file + }, + getComponentName = function() "Mock Component", + getOutputVarNames = function() c("var1", "var2") +) + +route = create_route(mock_model) +formatter = reqres::format_json(auto_unbox = TRUE) + +test_that("/get_component_name", { + fake_rook <- fiery::fake_request('/get_component_name') + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(list(name="Mock Component"))) +}) + +test_that("/get_output_var_names", { + fake_rook <- fiery::fake_request('/get_output_var_names') + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c("var1", "var2"))) +}) + +test_that("/initialize", { + fake_rook <- fiery::fake_request('/initialize' + , content = '{"config_file": "some_config"}' + , method = "post" + , headers = list("Content_Type" = "application/json") + ) + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 201) + expect_equal(bmi_initialize_called_with, "some_config") +}) \ No newline at end of file diff --git a/R/remotebmi/tests/testthat/test-server.R b/R/remotebmi/tests/testthat/test-server.R deleted file mode 100644 index 481e1da..0000000 --- a/R/remotebmi/tests/testthat/test-server.R +++ /dev/null @@ -1,4 +0,0 @@ -# TODO test routes -test_that("multiplication works", { - expect_equal(2 * 2, 4) -}) From 6529dbcc4deee3cff3bfbe608443fa648a6221c3 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 25 Sep 2024 13:38:25 +0200 Subject: [PATCH 03/27] Remove unused code --- r-server/R/server.R | 45 --------------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 r-server/R/server.R diff --git a/r-server/R/server.R b/r-server/R/server.R deleted file mode 100644 index 2b92634..0000000 --- a/r-server/R/server.R +++ /dev/null @@ -1,45 +0,0 @@ - -library(fiery) -library(routr) -library(reqres) - -#source('https://github.com/eWaterCycle/grpc4bmi-examples/raw/master/walrus/walrus-bmi.r') -#model <- WalrusBmi$new() - -get_component_name <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - # TODO call model method - response$body <- list(name = "Some model name") - response$format(json = format_json()) - return(FALSE) -} - -get_output_var_names <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - # TODO call model method - response$body <- c("var1", "var2", "var3") - response$format(json = format_json()) - return(FALSE) -} - -bmi_initialize <- function(request, response, keys, ...) { - request$parse(json = parse_json()) - model$bmi_initialize(request$body$config_file) - response$status <- 201L - return(FALSE) -} - -route <- Route$new() -route$add_handler('get', '/get_component_name', get_component_name) -route$add_handler('get', '/get_output_var_names', get_output_var_names) -route$add_handler('post', '/initialize', bmi_initialize) - -router <- RouteStack$new() -router$add_route(route, 'bmi') - -port = as.integer(Sys.getenv("BMI_PORT", 50051)) -app <- Fire$new(port=port) -app$attach(router) -app$ignite() From ec8242543494191a4cfb186afdddc283845cff32 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 25 Sep 2024 13:39:43 +0200 Subject: [PATCH 04/27] Sync docs --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 9cc4c9f..aa9f9f6 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,7 @@ library(remotebmi) library(MyModel) port = as.integer(Sys.getenv("BMI_PORT", 50051)) -server <- RemoteBmiServer(MyModel$ModelBmi, port=port, host="localhost") -server$run() +serve(MyModel::ModelBmi$new(), port=port, host="localhost") ``` ### Other languages From 412fd08fb71525e7d67a2f7e0dde9405aec01511 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 25 Sep 2024 13:48:28 +0200 Subject: [PATCH 05/27] Add ci for r + better docs --- .github/workflows/ci.yml | 15 +++++++++++++++ R/remotebmi/R/route.R | 3 +-- R/remotebmi/man/serve.Rd | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d864e65..7a09e00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,3 +54,18 @@ jobs: run: pip install ruff - name: Run ruff run: ruff check python/remotebmi + + r: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup R + uses: r-lib/actions/setup-r@v2 + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::rcmdcheck + needs: check + working-directory: R/remotebmi + - uses: r-lib/actions/check-r-package@v2 + working-directory: R/remotebmi \ No newline at end of file diff --git a/R/remotebmi/R/route.R b/R/remotebmi/R/route.R index fd31a8e..0bbf4c7 100644 --- a/R/remotebmi/R/route.R +++ b/R/remotebmi/R/route.R @@ -1,7 +1,7 @@ last_segment <- function(path) { - # keys get toLower at https://github.com/thomasp85/routr/blob/8605611a10607016a83660f83f310075787a27b2/R/route.R#L250 + # keys values are lowercase at https://github.com/thomasp85/routr/blob/8605611a10607016a83660f83f310075787a27b2/R/route.R#L250 # need untouched version segments <- unlist(strsplit(path, '/')) return(segments[length(segments)]) @@ -23,7 +23,6 @@ last_segment <- function(path) { #' route <- create_route(model) #' } #' -#' @export create_route <- function(model) { bmi_initialize <- function(request, response, keys, ...) { request$parse(json = reqres::parse_json()) diff --git a/R/remotebmi/man/serve.Rd b/R/remotebmi/man/serve.Rd index b4d9f58..af1f830 100644 --- a/R/remotebmi/man/serve.Rd +++ b/R/remotebmi/man/serve.Rd @@ -7,9 +7,9 @@ serve(model, port = 50051, host = "127.0.0.1", ignite = TRUE) } \arguments{ -\item{model}{The model to be served. Must implement the subclass of \href{https://github.com/eWaterCycle/bmi-r/blob/master/R/abstract-bmi.R}{AbstractBmi}} +\item{model}{The model instance to be served. Must implement the subclass of \href{https://github.com/eWaterCycle/bmi-r/blob/master/R/abstract-bmi.R}{AbstractBmi}} -\item{port}{The port to serve the model on. Default is 50051.} +\item{port}{The port to serve the model on. Default is 50051 or if BMI_PORT environment variable is set, it will be used.} \item{host}{The host to serve the model on. Default is "127.0.0.1".} From 49e1971aaeeffc8eb54fd708f7286de03a24f744 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 25 Sep 2024 13:50:56 +0200 Subject: [PATCH 06/27] tidy formatting --- R/remotebmi/R/route.R | 652 ++++++++++++------------ R/remotebmi/R/server.R | 22 +- R/remotebmi/tests/testthat/test-route.R | 65 ++- 3 files changed, 368 insertions(+), 371 deletions(-) diff --git a/R/remotebmi/R/route.R b/R/remotebmi/R/route.R index 0bbf4c7..cea5a84 100644 --- a/R/remotebmi/R/route.R +++ b/R/remotebmi/R/route.R @@ -1,343 +1,341 @@ - - last_segment <- function(path) { - # keys values are lowercase at https://github.com/thomasp85/routr/blob/8605611a10607016a83660f83f310075787a27b2/R/route.R#L250 - # need untouched version - segments <- unlist(strsplit(path, '/')) - return(segments[length(segments)]) + # keys values are lowercase at https://github.com/thomasp85/routr/blob/8605611a10607016a83660f83f310075787a27b2/R/route.R#L250 + # need untouched version + segments <- unlist(strsplit(path, "/")) + return(segments[length(segments)]) } #' Create a Route for the Given Model #' -#' This function generates a route for the specified model. The route is used to +#' This function generates a route for the specified model. The route is used to #' facilitate communication and interaction with the model. #' #' @param model The model instance to be used in route handlers Must implement the subclass of [AbstractBmi](https://github.com/eWaterCycle/bmi-r/blob/master/R/abstract-bmi.R) -#' +#' #' @return A route object that can be used to interact with the model. #' #' @examples #' \dontrun{ -#' model <- SomeModel$new() -#' route <- create_route(model) +#' model <- SomeModel$new() +#' route <- create_route(model) #' } #' create_route <- function(model) { - bmi_initialize <- function(request, response, keys, ...) { - request$parse(json = reqres::parse_json()) - model$bmi_initialize(request$body$config_file) - response$status <- 201L - return(FALSE) - } - - update <- function(request, response, keys, ...) { - model$update() - response$status <- 204L - return(FALSE) - } - - update_until <- function(request, response, keys, ...) { - request$parse(json = reqres::parse_json()) - until = request$body - model$updateUntil(until) - response$status <- 204L - return(FALSE) - } - - finalize <- function(request, response, keys, ...) { - model$bmi_finalize() - response$status <- 204L - return(FALSE) - } - - get_component_name <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- list(name = model$getComponentName()) - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_output_var_names <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getOutputVarNames() - response$format(json = reqres::format_json()) - return(FALSE) - } - - get_input_var_names <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getInputVarNames() - response$format(json = reqres::format_json()) - return(FALSE) - } - - get_time_units <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- list(units = model$getTimeUnits()) - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_time_step <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getTimeStep() - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_current_time <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getCurrentTime() - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_start_time <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getStartTime() - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_end_time <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getEndTime() - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_var_grid <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getVarGrid(last_segment(request$path)) - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_var_type <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - rawType <- model$getVarType(last_segment(request$path)) - type <- ifelse(rawType == 'float64', 'double', rawType) - # TODO map other types to double, float, int32 or int64 - response$body <- list(type = type) - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_var_itemsize <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getVarItemSize(last_segment(request$path)) - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_var_units <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- list(units = model$getVarUnits(last_segment(request$path))) - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_var_nbytes <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getVarNBytes(last_segment(request$path)) - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_value <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getValue(last_segment(request$path)) - response$format(json = reqres::format_json()) - return(FALSE) - } - - get_value_at_indices <- function(request, response, keys, ...) { - request$parse(json = reqres::parse_json()) - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getValueAtIndices(last_segment(request$path), request$body) - response$format(json = reqres::format_json()) - return(FALSE) - } - - set_value <- function(request, response, keys, ...) { - request$parse(json = reqres::parse_json()) - model$setValue(last_segment(request$path), request$body) - response$status <- 204L - return(FALSE) - } - - set_value_at_indices <- function(request, response, keys, ...) { - request$parse(json = reqres::parse_json()) - model$setValueAtIndices(last_segment(request$path), request$body$indices, request$body$values) - response$status <- 204L - return(FALSE) - } - - get_grid_rank <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getGridRank(keys$grid) - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_grid_type <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- list(type = model$getGridType(keys$grid)) - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_grid_size <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getGridSize(keys$grid) - response$format(json = reqres::format_json(auto_unbox=TRUE)) - return(FALSE) - } - - get_grid_x <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getGridX(keys$grid) - response$format(json = reqres::format_json()) - return(FALSE) - } - - get_grid_y <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getGridY(keys$grid) - response$format(json = reqres::format_json()) - return(FALSE) - } - - get_grid_z <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getGridZ(keys$grid) - response$format(json = reqres::format_json()) - return(FALSE) - } - - get_grid_origin <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getGridOrigin(keys$grid) - response$format(json = reqres::format_json()) - return(FALSE) - } - - get_grid_shape <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getGridShape(keys$grid) - response$format(json = reqres::format_json()) - return(FALSE) - } - - get_grid_spacing <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getGridSpacing(keys$grid) - response$format(json = reqres::format_json()) - return(FALSE) - } - - get_grid_connectivity <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getGridConnectivity(keys$grid) - response$format(json = reqres::format_json()) - return(FALSE) - } - - get_grid_offset <- function(request, response, keys, ...) { - response$status <- 200L - response$type <- 'application/json' - response$body <- model$getGridOffset(keys$grid) - response$format(json = reqres::format_json()) - return(FALSE) - } - - route <- routr::Route$new() - # IRF - route$add_handler('post', '/initialize', bmi_initialize) - route$add_handler('post', '/update', update) - route$add_handler('post', '/update_until', update_until) - route$add_handler('delete', '/finalize', finalize) - - # Exchange items - route$add_handler('get', '/get_component_name', get_component_name) - route$add_handler('get', '/get_output_var_names', get_output_var_names) - route$add_handler('get', '/get_input_var_names', get_input_var_names) - - # Getters - route$add_handler('get', '/get_value/:name', get_value) - route$add_handler('get', '/get_value_at_indices/:name', get_value_at_indices) - - # Setters - route$add_handler('post', '/set_value/:name', set_value) - route$add_handler('post', '/set_value_at_indices/:name', set_value_at_indices) - - # Time information - route$add_handler('get', '/get_time_units', get_time_units) - route$add_handler('get', '/get_time_step', get_time_step) - route$add_handler('get', '/get_current_time', get_current_time) - route$add_handler('get', '/get_start_time', get_start_time) - route$add_handler('get', '/get_end_time', get_end_time) - - # Variable information - route$add_handler('get', '/get_var_grid/:name', get_var_grid) - route$add_handler('get', '/get_var_type/:name', get_var_type) - route$add_handler('get', '/get_var_itemsize/:name', get_var_itemsize) - route$add_handler('get', '/get_var_units/:name', get_var_units) - route$add_handler('get', '/get_var_nbytes/:name', get_var_nbytes) - - # Grid information - route$add_handler('get', '/get_grid_rank/:grid', get_grid_rank) - route$add_handler('get', '/get_grid_type/:grid', get_grid_type) - route$add_handler('get', '/get_grid_size/:grid', get_grid_size) - - # NURC - route$add_handler('get', '/get_grid_x/:grid', get_grid_x) - route$add_handler('get', '/get_grid_y/:grid', get_grid_y) - route$add_handler('get', '/get_grid_z/:grid', get_grid_z) - - # Uniform rectilinear - route$add_handler('get', '/get_grid_origin/:grid', get_grid_origin) - route$add_handler('get', '/get_grid_shape/:grid', get_grid_shape) - route$add_handler('get', '/get_grid_spacing/:grid', get_grid_spacing) - - # Unstructured - route$add_handler('get', '/get_grid_connectivity/:grid', get_grid_connectivity) - route$add_handler('get', '/get_grid_offset/:grid', get_grid_offset) - - # TODO Needed? - hFallback <- function(request, response, keys, ...) { - response$status <- 404L - response$type <- 'text/plain' - response$body <- 'Not found' - return(FALSE) - } - route$add_handler('get', '/*', hFallback) - - return(route) -} \ No newline at end of file + bmi_initialize <- function(request, response, keys, ...) { + request$parse(json = reqres::parse_json()) + model$bmi_initialize(request$body$config_file) + response$status <- 201L + return(FALSE) + } + + update <- function(request, response, keys, ...) { + model$update() + response$status <- 204L + return(FALSE) + } + + update_until <- function(request, response, keys, ...) { + request$parse(json = reqres::parse_json()) + until <- request$body + model$updateUntil(until) + response$status <- 204L + return(FALSE) + } + + finalize <- function(request, response, keys, ...) { + model$bmi_finalize() + response$status <- 204L + return(FALSE) + } + + get_component_name <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- list(name = model$getComponentName()) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_output_var_names <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getOutputVarNames() + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_input_var_names <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getInputVarNames() + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_time_units <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- list(units = model$getTimeUnits()) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_time_step <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getTimeStep() + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_current_time <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getCurrentTime() + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_start_time <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getStartTime() + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_end_time <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getEndTime() + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_var_grid <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getVarGrid(last_segment(request$path)) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_var_type <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + rawType <- model$getVarType(last_segment(request$path)) + type <- ifelse(rawType == "float64", "double", rawType) + # TODO map other types to double, float, int32 or int64 + response$body <- list(type = type) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_var_itemsize <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getVarItemSize(last_segment(request$path)) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_var_units <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- list(units = model$getVarUnits(last_segment(request$path))) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_var_nbytes <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getVarNBytes(last_segment(request$path)) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_value <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getValue(last_segment(request$path)) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_value_at_indices <- function(request, response, keys, ...) { + request$parse(json = reqres::parse_json()) + response$status <- 200L + response$type <- "application/json" + response$body <- model$getValueAtIndices(last_segment(request$path), request$body) + response$format(json = reqres::format_json()) + return(FALSE) + } + + set_value <- function(request, response, keys, ...) { + request$parse(json = reqres::parse_json()) + model$setValue(last_segment(request$path), request$body) + response$status <- 204L + return(FALSE) + } + + set_value_at_indices <- function(request, response, keys, ...) { + request$parse(json = reqres::parse_json()) + model$setValueAtIndices(last_segment(request$path), request$body$indices, request$body$values) + response$status <- 204L + return(FALSE) + } + + get_grid_rank <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridRank(keys$grid) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_grid_type <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- list(type = model$getGridType(keys$grid)) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_grid_size <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridSize(keys$grid) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + + get_grid_x <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridX(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_y <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridY(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_z <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridZ(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_origin <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridOrigin(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_shape <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridShape(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_spacing <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridSpacing(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_connectivity <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridConnectivity(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_offset <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridOffset(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + route <- routr::Route$new() + # IRF + route$add_handler("post", "/initialize", bmi_initialize) + route$add_handler("post", "/update", update) + route$add_handler("post", "/update_until", update_until) + route$add_handler("delete", "/finalize", finalize) + + # Exchange items + route$add_handler("get", "/get_component_name", get_component_name) + route$add_handler("get", "/get_output_var_names", get_output_var_names) + route$add_handler("get", "/get_input_var_names", get_input_var_names) + + # Getters + route$add_handler("get", "/get_value/:name", get_value) + route$add_handler("get", "/get_value_at_indices/:name", get_value_at_indices) + + # Setters + route$add_handler("post", "/set_value/:name", set_value) + route$add_handler("post", "/set_value_at_indices/:name", set_value_at_indices) + + # Time information + route$add_handler("get", "/get_time_units", get_time_units) + route$add_handler("get", "/get_time_step", get_time_step) + route$add_handler("get", "/get_current_time", get_current_time) + route$add_handler("get", "/get_start_time", get_start_time) + route$add_handler("get", "/get_end_time", get_end_time) + + # Variable information + route$add_handler("get", "/get_var_grid/:name", get_var_grid) + route$add_handler("get", "/get_var_type/:name", get_var_type) + route$add_handler("get", "/get_var_itemsize/:name", get_var_itemsize) + route$add_handler("get", "/get_var_units/:name", get_var_units) + route$add_handler("get", "/get_var_nbytes/:name", get_var_nbytes) + + # Grid information + route$add_handler("get", "/get_grid_rank/:grid", get_grid_rank) + route$add_handler("get", "/get_grid_type/:grid", get_grid_type) + route$add_handler("get", "/get_grid_size/:grid", get_grid_size) + + # NURC + route$add_handler("get", "/get_grid_x/:grid", get_grid_x) + route$add_handler("get", "/get_grid_y/:grid", get_grid_y) + route$add_handler("get", "/get_grid_z/:grid", get_grid_z) + + # Uniform rectilinear + route$add_handler("get", "/get_grid_origin/:grid", get_grid_origin) + route$add_handler("get", "/get_grid_shape/:grid", get_grid_shape) + route$add_handler("get", "/get_grid_spacing/:grid", get_grid_spacing) + + # Unstructured + route$add_handler("get", "/get_grid_connectivity/:grid", get_grid_connectivity) + route$add_handler("get", "/get_grid_offset/:grid", get_grid_offset) + + # TODO Needed? + hFallback <- function(request, response, keys, ...) { + response$status <- 404L + response$type <- "text/plain" + response$body <- "Not found" + return(FALSE) + } + route$add_handler("get", "/*", hFallback) + + return(route) +} diff --git a/R/remotebmi/R/server.R b/R/remotebmi/R/server.R index aa50a08..a45540c 100644 --- a/R/remotebmi/R/server.R +++ b/R/remotebmi/R/server.R @@ -13,15 +13,15 @@ library(reqres) #' @return The server application. #' @export serve <- function(model, port = 50051, host = "127.0.0.1", ignite = TRUE) { - route = create_route(model) - router <- routr::RouteStack$new() - router$add_route(route, 'bmi') + route <- create_route(model) + router <- routr::RouteStack$new() + router$add_route(route, "bmi") - port = as.integer(Sys.getenv("BMI_PORT", port)) - app <- fiery::Fire$new(host=host, port=port) - app$attach(router) - if (ignite) { - app$ignite() - } - return(app) -} \ No newline at end of file + port <- as.integer(Sys.getenv("BMI_PORT", port)) + app <- fiery::Fire$new(host = host, port = port) + app$attach(router) + if (ignite) { + app$ignite() + } + return(app) +} diff --git a/R/remotebmi/tests/testthat/test-route.R b/R/remotebmi/tests/testthat/test-route.R index 93b986e..aa258bb 100644 --- a/R/remotebmi/tests/testthat/test-route.R +++ b/R/remotebmi/tests/testthat/test-route.R @@ -1,45 +1,44 @@ - # Poor mans mock -bmi_initialize_called_with <<- '' +bmi_initialize_called_with <<- "" # Mock model object -mock_model <- list( - bmi_initialize = function(config_file) { - bmi_initialize_called_with <<- config_file - }, - getComponentName = function() "Mock Component", - getOutputVarNames = function() c("var1", "var2") +mock_model <- list( + bmi_initialize = function(config_file) { + bmi_initialize_called_with <<- config_file + }, + getComponentName = function() "Mock Component", + getOutputVarNames = function() c("var1", "var2") ) -route = create_route(mock_model) -formatter = reqres::format_json(auto_unbox = TRUE) +route <- create_route(mock_model) +formatter <- reqres::format_json(auto_unbox = TRUE) test_that("/get_component_name", { - fake_rook <- fiery::fake_request('/get_component_name') - req <- reqres::Request$new(fake_rook) - res <- req$respond() - route$dispatch(req) - expect_equal(res$status, 200) - expect_equal(res$body, formatter(list(name="Mock Component"))) + fake_rook <- fiery::fake_request("/get_component_name") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(list(name = "Mock Component"))) }) test_that("/get_output_var_names", { - fake_rook <- fiery::fake_request('/get_output_var_names') - req <- reqres::Request$new(fake_rook) - res <- req$respond() - route$dispatch(req) - expect_equal(res$status, 200) - expect_equal(res$body, formatter(c("var1", "var2"))) + fake_rook <- fiery::fake_request("/get_output_var_names") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c("var1", "var2"))) }) test_that("/initialize", { - fake_rook <- fiery::fake_request('/initialize' - , content = '{"config_file": "some_config"}' - , method = "post" - , headers = list("Content_Type" = "application/json") - ) - req <- reqres::Request$new(fake_rook) - res <- req$respond() - route$dispatch(req) - expect_equal(res$status, 201) - expect_equal(bmi_initialize_called_with, "some_config") -}) \ No newline at end of file + fake_rook <- fiery::fake_request("/initialize", + content = '{"config_file": "some_config"}', + method = "post", + headers = list("Content_Type" = "application/json") + ) + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 201) + expect_equal(bmi_initialize_called_with, "some_config") +}) From 815549482d105951bc5d55b7b321a34fda7658d7 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 25 Sep 2024 14:05:33 +0200 Subject: [PATCH 07/27] Split CI per lang --- .github/workflows/{ci.yml => openapi.yml} | 33 +---------------------- .github/workflows/python.yml | 28 +++++++++++++++++++ .github/workflows/r.yml | 28 +++++++++++++++++++ 3 files changed, 57 insertions(+), 32 deletions(-) rename .github/workflows/{ci.yml => openapi.yml} (51%) create mode 100644 .github/workflows/python.yml create mode 100644 .github/workflows/r.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/openapi.yml similarity index 51% rename from .github/workflows/ci.yml rename to .github/workflows/openapi.yml index 7a09e00..8f0ebbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/openapi.yml @@ -1,4 +1,4 @@ -name: Validate OpenAPI +name: Validate OpenAPI.yaml on: push: @@ -38,34 +38,3 @@ jobs: - name: Yamllint run: yamllint openapi.yaml - - python: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: "pip" - - name: Install ruff - run: pip install ruff - - name: Run ruff - run: ruff check python/remotebmi - - r: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Setup R - uses: r-lib/actions/setup-r@v2 - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - extra-packages: any::rcmdcheck - needs: check - working-directory: R/remotebmi - - uses: r-lib/actions/check-r-package@v2 - working-directory: R/remotebmi \ No newline at end of file diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..936aed1 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,28 @@ +name: Python + +on: + push: + paths: + - "openapi.yaml" + - "python/remotebmi/**" + pull_request: + paths: + - "openapi.yaml" + - "python/remotebmi/**" + +jobs: + python: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + - name: Install ruff + run: pip install ruff + - name: Run ruff + run: ruff check python/remotebmi \ No newline at end of file diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml new file mode 100644 index 0000000..f618348 --- /dev/null +++ b/.github/workflows/r.yml @@ -0,0 +1,28 @@ +name: R + +on: + push: + paths: + - "R/remotebmi/**" + pull_request: + paths: + - "R/remotebmi/**" + +jobs: + r: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + sparse-checkout: | + R/remotebmi + - name: Move R package to root + run: shopt -s dotglob && mv R/remotebmi/* . + - name: Setup R + uses: r-lib/actions/setup-r@v2 + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::rcmdcheck + needs: check + - uses: r-lib/actions/check-r-package@v2 From e94f3e02ff4451cab43658a2402d12d92a0593b4 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 25 Sep 2024 14:14:12 +0200 Subject: [PATCH 08/27] Add test --- R/remotebmi/tests/testthat/test-route.R | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/R/remotebmi/tests/testthat/test-route.R b/R/remotebmi/tests/testthat/test-route.R index aa258bb..44fff72 100644 --- a/R/remotebmi/tests/testthat/test-route.R +++ b/R/remotebmi/tests/testthat/test-route.R @@ -1,12 +1,17 @@ # Poor mans mock bmi_initialize_called_with <<- "" +bmi_get_var_units_called_with <<- "" # Mock model object mock_model <- list( bmi_initialize = function(config_file) { bmi_initialize_called_with <<- config_file }, getComponentName = function() "Mock Component", - getOutputVarNames = function() c("var1", "var2") + getOutputVarNames = function() c("var1", "var2"), + getVarUnits = function(name) { + bmi_get_var_units_called_with <<- name + return("unit1") + } ) route <- create_route(mock_model) @@ -42,3 +47,13 @@ test_that("/initialize", { expect_equal(res$status, 201) expect_equal(bmi_initialize_called_with, "some_config") }) + +test_that("/get_var_units", { + fake_rook <- fiery::fake_request("/get_var_units/Q") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(list(units = "unit1"))) + expect_equal(bmi_get_var_units_called_with, "Q") +}) From 7f6ea89778783eea54b909bbe21cbc22bcb7d8af Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 25 Sep 2024 14:14:19 +0200 Subject: [PATCH 09/27] Also run ci when ci changes --- .github/workflows/openapi.yml | 2 ++ .github/workflows/python.yml | 2 ++ .github/workflows/r.yml | 7 +++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 8f0ebbd..61b15b9 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -4,9 +4,11 @@ on: push: paths: - "openapi.yaml" + - .github/workflows/openapi.yml pull_request: paths: - "openapi.yaml" + - .github/workflows/openapi.yml jobs: lintspec: diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 936aed1..983b0d3 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -5,10 +5,12 @@ on: paths: - "openapi.yaml" - "python/remotebmi/**" + - .github/workflows/python.yml pull_request: paths: - "openapi.yaml" - "python/remotebmi/**" + - .github/workflows/python.yml jobs: python: diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index f618348..1feb817 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -4,9 +4,11 @@ on: push: paths: - "R/remotebmi/**" + - .github/workflows/r.yml pull_request: paths: - "R/remotebmi/**" + - .github/workflows/r.yml jobs: r: @@ -17,8 +19,9 @@ jobs: with: sparse-checkout: | R/remotebmi - - name: Move R package to root - run: shopt -s dotglob && mv R/remotebmi/* . + path: root + - name: Move R package to cwd + run: shopt -s dotglob && mv root/R/remotebmi/* . - name: Setup R uses: r-lib/actions/setup-r@v2 - uses: r-lib/actions/setup-r-dependencies@v2 From 35c4dd8b53b7ab83b4a65e9b7363dac264c3e816 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 25 Sep 2024 14:23:02 +0200 Subject: [PATCH 10/27] Run ci less --- .github/workflows/openapi.yml | 6 +++++- .github/workflows/python.yml | 6 +++++- .github/workflows/r.yml | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 61b15b9..52ac8c6 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -5,11 +5,15 @@ on: paths: - "openapi.yaml" - .github/workflows/openapi.yml - pull_request: + pull_request_target: paths: - "openapi.yaml" - .github/workflows/openapi.yml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: lintspec: runs-on: ubuntu-latest diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 983b0d3..60de449 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -6,12 +6,16 @@ on: - "openapi.yaml" - "python/remotebmi/**" - .github/workflows/python.yml - pull_request: + pull_request_target: paths: - "openapi.yaml" - "python/remotebmi/**" - .github/workflows/python.yml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: python: runs-on: ubuntu-latest diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index 1feb817..15e17b1 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -5,11 +5,15 @@ on: paths: - "R/remotebmi/**" - .github/workflows/r.yml - pull_request: + pull_request_target: paths: - "R/remotebmi/**" - .github/workflows/r.yml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: r: runs-on: ubuntu-latest From 4f81eeb1ba3aed91bade0c15ae650e00df3019ff Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Wed, 25 Sep 2024 14:55:34 +0200 Subject: [PATCH 11/27] Add missing routes --- R/remotebmi/R/route.R | 111 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 95 insertions(+), 16 deletions(-) diff --git a/R/remotebmi/R/route.R b/R/remotebmi/R/route.R index cea5a84..ab9f55d 100644 --- a/R/remotebmi/R/route.R +++ b/R/remotebmi/R/route.R @@ -1,5 +1,6 @@ last_segment <- function(path) { - # keys values are lowercase at https://github.com/thomasp85/routr/blob/8605611a10607016a83660f83f310075787a27b2/R/route.R#L250 + # keys values are lowercase at + # https://github.com/thomasp85/routr/blob/8605611a10607016a83660f83f310075787a27b2/R/route.R#L250 # nolint: line_length_linter. # need untouched version segments <- unlist(strsplit(path, "/")) return(segments[length(segments)]) @@ -11,7 +12,9 @@ last_segment <- function(path) { #' This function generates a route for the specified model. The route is used to #' facilitate communication and interaction with the model. #' -#' @param model The model instance to be used in route handlers Must implement the subclass of [AbstractBmi](https://github.com/eWaterCycle/bmi-r/blob/master/R/abstract-bmi.R) +#' @param model The model instance to be used in route handlers. +#' Must implement the subclass of +#' [AbstractBmi](https://github.com/eWaterCycle/bmi-r/blob/master/R/abstract-bmi.R) # nolint: line_length_linter. #' #' @return A route object that can be used to interact with the model. #' @@ -37,8 +40,8 @@ create_route <- function(model) { update_until <- function(request, response, keys, ...) { request$parse(json = reqres::parse_json()) - until <- request$body - model$updateUntil(until) + time <- request$body + model$updateUntil(time) response$status <- 204L return(FALSE) } @@ -65,6 +68,14 @@ create_route <- function(model) { return(FALSE) } + get_output_item_count <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getOutputItemCount() + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + get_input_var_names <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" @@ -73,6 +84,14 @@ create_route <- function(model) { return(FALSE) } + get_input_item_count <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getInputItemCount() + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + get_time_units <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" @@ -124,7 +143,8 @@ create_route <- function(model) { get_var_type <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - rawType <- model$getVarType(last_segment(request$path)) + name <- last_segment(request$path) + rawType <- model$getVarType(name) # nolint: object_name_linter. type <- ifelse(rawType == "float64", "double", rawType) # TODO map other types to double, float, int32 or int64 response$body <- list(type = type) @@ -156,6 +176,15 @@ create_route <- function(model) { return(FALSE) } + get_var_location <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + name <- last_segment(request$path) + response$body <- list(location = model$getVarLocation(name)) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + get_value <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" @@ -168,7 +197,8 @@ create_route <- function(model) { request$parse(json = reqres::parse_json()) response$status <- 200L response$type <- "application/json" - response$body <- model$getValueAtIndices(last_segment(request$path), request$body) + name <- last_segment(request$path) + response$body <- model$getValueAtIndices(name, request$body) response$format(json = reqres::format_json()) return(FALSE) } @@ -182,7 +212,8 @@ create_route <- function(model) { set_value_at_indices <- function(request, response, keys, ...) { request$parse(json = reqres::parse_json()) - model$setValueAtIndices(last_segment(request$path), request$body$indices, request$body$values) + name <- last_segment(request$path) + model$setValueAtIndices(name, request$body$indices, request$body$values) response$status <- 204L return(FALSE) } @@ -259,18 +290,58 @@ create_route <- function(model) { return(FALSE) } - get_grid_connectivity <- function(request, response, keys, ...) { + get_grid_node_count <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridNodeCount(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_edge_count <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridEdgeCount(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_face_count <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridFaceCount(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_edge_nodes <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridEdgeNodes(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_face_edges <- function(request, response, keys, ...) { + response$status <- 200L + response$type <- "application/json" + response$body <- model$getGridFaceEdges(keys$grid) + response$format(json = reqres::format_json()) + return(FALSE) + } + + get_grid_face_nodes <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridConnectivity(keys$grid) + response$body <- model$getGridFaceNodes(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } - get_grid_offset <- function(request, response, keys, ...) { + get_grid_nodes_per_face <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridOffset(keys$grid) + response$body <- model$getGridNodesPerFace(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -285,7 +356,9 @@ create_route <- function(model) { # Exchange items route$add_handler("get", "/get_component_name", get_component_name) route$add_handler("get", "/get_output_var_names", get_output_var_names) + route$add_handler("get", "/get_output_item_count", get_output_item_count) route$add_handler("get", "/get_input_var_names", get_input_var_names) + route$add_handler("get", "/get_input_item_count", get_input_item_count) # Getters route$add_handler("get", "/get_value/:name", get_value) @@ -308,6 +381,7 @@ create_route <- function(model) { route$add_handler("get", "/get_var_itemsize/:name", get_var_itemsize) route$add_handler("get", "/get_var_units/:name", get_var_units) route$add_handler("get", "/get_var_nbytes/:name", get_var_nbytes) + route$add_handler("get", "/get_var_location/:name", get_var_location) # Grid information route$add_handler("get", "/get_grid_rank/:grid", get_grid_rank) @@ -324,18 +398,23 @@ create_route <- function(model) { route$add_handler("get", "/get_grid_shape/:grid", get_grid_shape) route$add_handler("get", "/get_grid_spacing/:grid", get_grid_spacing) - # Unstructured - route$add_handler("get", "/get_grid_connectivity/:grid", get_grid_connectivity) - route$add_handler("get", "/get_grid_offset/:grid", get_grid_offset) + # # Unstructured + route$add_handler("get", "/get_grid_node_count/:grid", get_grid_node_count) + route$add_handler("get", "/get_grid_edge_count/:grid", get_grid_edge_count) + route$add_handler("get", "/get_grid_face_count/:grid", get_grid_face_count) + route$add_handler("get", "/get_grid_edge_nodes/:grid", get_grid_edge_nodes) + route$add_handler("get", "/get_grid_face_edges/:grid", get_grid_face_edges) + route$add_handler("get", "/get_grid_face_nodes/:grid", get_grid_face_nodes) + route$add_handler("get", "/get_grid_nodes_per_face/:grid", get_grid_nodes_per_face) # nolint: line_length_linter. # TODO Needed? - hFallback <- function(request, response, keys, ...) { + fallback <- function(request, response, keys, ...) { response$status <- 404L response$type <- "text/plain" response$body <- "Not found" return(FALSE) } - route$add_handler("get", "/*", hFallback) + route$add_handler("get", "/*", fallback) return(route) } From f016e01eed50f4056d415c4048cdbeee003593b7 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 11:36:43 +0200 Subject: [PATCH 12/27] Use httpx client without keep alive connections --- R/remotebmi/README.md | 2 -- python/remotebmi/client/client.py | 9 ++++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/R/remotebmi/README.md b/R/remotebmi/README.md index 9710f76..2e78b91 100644 --- a/R/remotebmi/README.md +++ b/R/remotebmi/README.md @@ -68,8 +68,6 @@ client.get_value('Q', dest=np.array([.1])) array([0.0044]) client.get_var_nbytes('Q') 'mm/h' -# TODO get_var_nbytes should return int not str -# this breaks reserve_values() aswell dest = reserve_values(client, 'Q') r = client.get_value('Q', dest) r diff --git a/python/remotebmi/client/client.py b/python/remotebmi/client/client.py index 738bb5c..1c1a434 100644 --- a/python/remotebmi/client/client.py +++ b/python/remotebmi/client/client.py @@ -1,12 +1,15 @@ import numpy as np from bmipy import Bmi -from httpx import Client +from httpx import Client, Limits from numpy import ndarray class RemoteBmiClient(Bmi): - def __init__(self, base_url): - self.client = Client(base_url=base_url) + def __init__(self, base_url, max_keepalive_connections=0): + # In some Python environments the reusing connection causes `illegal status line: bytesarray(b'14')` error + # So we need to disable keepalive connections to be more reliable, but less efficient + limits = Limits(max_keepalive_connections=max_keepalive_connections) + self.client = Client(base_url=base_url, limits=limits) def __del__(self): self.client.close() From 73e3a9aceec3b78ea126e012c5197c512258b888 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 13:19:45 +0200 Subject: [PATCH 13/27] Use _ in R bmi --- R/remotebmi/R/route.R | 74 ++++++++++++------------- R/remotebmi/tests/testthat/test-route.R | 29 ++++++---- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/R/remotebmi/R/route.R b/R/remotebmi/R/route.R index ab9f55d..dbc26d4 100644 --- a/R/remotebmi/R/route.R +++ b/R/remotebmi/R/route.R @@ -41,7 +41,7 @@ create_route <- function(model) { update_until <- function(request, response, keys, ...) { request$parse(json = reqres::parse_json()) time <- request$body - model$updateUntil(time) + model$update_until(time) response$status <- 204L return(FALSE) } @@ -55,7 +55,7 @@ create_route <- function(model) { get_component_name <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- list(name = model$getComponentName()) + response$body <- list(name = model$get_component_name()) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -63,7 +63,7 @@ create_route <- function(model) { get_output_var_names <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getOutputVarNames() + response$body <- model$get_output_var_names() response$format(json = reqres::format_json()) return(FALSE) } @@ -71,7 +71,7 @@ create_route <- function(model) { get_output_item_count <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getOutputItemCount() + response$body <- model$get_output_item_count() response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -79,7 +79,7 @@ create_route <- function(model) { get_input_var_names <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getInputVarNames() + response$body <- model$get_input_var_names() response$format(json = reqres::format_json()) return(FALSE) } @@ -87,7 +87,7 @@ create_route <- function(model) { get_input_item_count <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getInputItemCount() + response$body <- model$get_input_item_count() response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -95,7 +95,7 @@ create_route <- function(model) { get_time_units <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- list(units = model$getTimeUnits()) + response$body <- list(units = model$get_time_units()) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -103,7 +103,7 @@ create_route <- function(model) { get_time_step <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getTimeStep() + response$body <- model$get_time_step() response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -111,7 +111,7 @@ create_route <- function(model) { get_current_time <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getCurrentTime() + response$body <- model$get_current_time() response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -119,7 +119,7 @@ create_route <- function(model) { get_start_time <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getStartTime() + response$body <- model$get_start_time() response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -127,7 +127,7 @@ create_route <- function(model) { get_end_time <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getEndTime() + response$body <- model$get_end_time() response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -135,7 +135,7 @@ create_route <- function(model) { get_var_grid <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getVarGrid(last_segment(request$path)) + response$body <- model$get_var_grid(last_segment(request$path)) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -144,7 +144,7 @@ create_route <- function(model) { response$status <- 200L response$type <- "application/json" name <- last_segment(request$path) - rawType <- model$getVarType(name) # nolint: object_name_linter. + rawType <- model$get_var_type(name) # nolint: object_name_linter. type <- ifelse(rawType == "float64", "double", rawType) # TODO map other types to double, float, int32 or int64 response$body <- list(type = type) @@ -155,7 +155,7 @@ create_route <- function(model) { get_var_itemsize <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getVarItemSize(last_segment(request$path)) + response$body <- model$get_var_itemsize(last_segment(request$path)) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -163,7 +163,7 @@ create_route <- function(model) { get_var_units <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- list(units = model$getVarUnits(last_segment(request$path))) + response$body <- list(units = model$get_var_units(last_segment(request$path))) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -171,7 +171,7 @@ create_route <- function(model) { get_var_nbytes <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getVarNBytes(last_segment(request$path)) + response$body <- model$get_var_nbytes(last_segment(request$path)) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -180,7 +180,7 @@ create_route <- function(model) { response$status <- 200L response$type <- "application/json" name <- last_segment(request$path) - response$body <- list(location = model$getVarLocation(name)) + response$body <- list(location = model$get_var_location(name)) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -188,7 +188,7 @@ create_route <- function(model) { get_value <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getValue(last_segment(request$path)) + response$body <- model$get_value(last_segment(request$path)) response$format(json = reqres::format_json()) return(FALSE) } @@ -198,14 +198,14 @@ create_route <- function(model) { response$status <- 200L response$type <- "application/json" name <- last_segment(request$path) - response$body <- model$getValueAtIndices(name, request$body) + response$body <- model$get_value_at_indices(name, request$body) response$format(json = reqres::format_json()) return(FALSE) } set_value <- function(request, response, keys, ...) { request$parse(json = reqres::parse_json()) - model$setValue(last_segment(request$path), request$body) + model$set_value(last_segment(request$path), request$body) response$status <- 204L return(FALSE) } @@ -213,7 +213,7 @@ create_route <- function(model) { set_value_at_indices <- function(request, response, keys, ...) { request$parse(json = reqres::parse_json()) name <- last_segment(request$path) - model$setValueAtIndices(name, request$body$indices, request$body$values) + model$set_value_at_indices(name, request$body$indices, request$body$values) response$status <- 204L return(FALSE) } @@ -221,7 +221,7 @@ create_route <- function(model) { get_grid_rank <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridRank(keys$grid) + response$body <- model$get_grid_rank(keys$grid) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -229,7 +229,7 @@ create_route <- function(model) { get_grid_type <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- list(type = model$getGridType(keys$grid)) + response$body <- list(type = model$get_grid_type(keys$grid)) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -237,7 +237,7 @@ create_route <- function(model) { get_grid_size <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridSize(keys$grid) + response$body <- model$get_grid_size(keys$grid) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -245,7 +245,7 @@ create_route <- function(model) { get_grid_x <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridX(keys$grid) + response$body <- model$get_grid_x(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -253,7 +253,7 @@ create_route <- function(model) { get_grid_y <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridY(keys$grid) + response$body <- model$get_grid_y(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -261,7 +261,7 @@ create_route <- function(model) { get_grid_z <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridZ(keys$grid) + response$body <- model$get_grid_z(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -269,7 +269,7 @@ create_route <- function(model) { get_grid_origin <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridOrigin(keys$grid) + response$body <- model$get_grid_origin(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -277,7 +277,7 @@ create_route <- function(model) { get_grid_shape <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridShape(keys$grid) + response$body <- model$get_grid_shape(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -285,7 +285,7 @@ create_route <- function(model) { get_grid_spacing <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridSpacing(keys$grid) + response$body <- model$get_grid_spacing(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -293,7 +293,7 @@ create_route <- function(model) { get_grid_node_count <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridNodeCount(keys$grid) + response$body <- model$get_grid_node_count(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -301,7 +301,7 @@ create_route <- function(model) { get_grid_edge_count <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridEdgeCount(keys$grid) + response$body <- model$get_grid_edge_count(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -309,7 +309,7 @@ create_route <- function(model) { get_grid_face_count <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridFaceCount(keys$grid) + response$body <- model$get_grid_face_count(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -317,7 +317,7 @@ create_route <- function(model) { get_grid_edge_nodes <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridEdgeNodes(keys$grid) + response$body <- model$get_grid_edge_nodes(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -325,7 +325,7 @@ create_route <- function(model) { get_grid_face_edges <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridFaceEdges(keys$grid) + response$body <- model$get_grid_face_edges(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -333,7 +333,7 @@ create_route <- function(model) { get_grid_face_nodes <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridFaceNodes(keys$grid) + response$body <- model$get_grid_face_nodes(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } @@ -341,7 +341,7 @@ create_route <- function(model) { get_grid_nodes_per_face <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$getGridNodesPerFace(keys$grid) + response$body <- model$get_grid_nodes_per_face(keys$grid) response$format(json = reqres::format_json()) return(FALSE) } diff --git a/R/remotebmi/tests/testthat/test-route.R b/R/remotebmi/tests/testthat/test-route.R index 44fff72..e6338f7 100644 --- a/R/remotebmi/tests/testthat/test-route.R +++ b/R/remotebmi/tests/testthat/test-route.R @@ -1,18 +1,27 @@ +library(R6) +library(bmi) + # Poor mans mock bmi_initialize_called_with <<- "" bmi_get_var_units_called_with <<- "" # Mock model object -mock_model <- list( - bmi_initialize = function(config_file) { - bmi_initialize_called_with <<- config_file - }, - getComponentName = function() "Mock Component", - getOutputVarNames = function() c("var1", "var2"), - getVarUnits = function(name) { - bmi_get_var_units_called_with <<- name - return("unit1") - } +# Modelled after +# https://github.com/eWaterCycle/grpc4bmi/blob/main/test/fake_models.py +MockedBmi <- R6Class("MockedBmi", + inherit = AbstractBmi, + public = list( + bmi_initialize = function(config_file) { + bmi_initialize_called_with <<- config_file + }, + get_component_name = function() "Mock Component", + get_output_var_names = function() c("var1", "var2"), + get_var_units = function(name) { + bmi_get_var_units_called_with <<- name + return("unit1") + } + ) ) +mock_model <- MockedBmi$new() route <- create_route(mock_model) formatter <- reqres::format_json(auto_unbox = TRUE) From fad9f4d971320b462a62f78c2d6838206b16f06e Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 14:24:31 +0200 Subject: [PATCH 14/27] Test all walrus methods, all ok except get_input_var_names --- R/remotebmi/R/route.R | 2 +- R/remotebmi/README.md | 65 ++++++++++++++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/R/remotebmi/R/route.R b/R/remotebmi/R/route.R index dbc26d4..3337c93 100644 --- a/R/remotebmi/R/route.R +++ b/R/remotebmi/R/route.R @@ -362,7 +362,7 @@ create_route <- function(model) { # Getters route$add_handler("get", "/get_value/:name", get_value) - route$add_handler("get", "/get_value_at_indices/:name", get_value_at_indices) + route$add_handler("post", "/get_value_at_indices/:name", get_value_at_indices) # Setters route$add_handler("post", "/set_value/:name", set_value) diff --git a/R/remotebmi/README.md b/R/remotebmi/README.md index 2e78b91..245474b 100644 --- a/R/remotebmi/README.md +++ b/R/remotebmi/README.md @@ -38,7 +38,7 @@ With Python client test the model ```python import os from remotebmi.client.client import RemoteBmiClient -from remotebmi.reserve import reserve_values +from remotebmi.reserve import reserve_values, reserve_grid_padding, reserve_grid_shape import numpy as np client = RemoteBmiClient('http://localhost:50051') @@ -46,31 +46,62 @@ client = RemoteBmiClient('http://localhost:50051') !wget https://github.com/ClaudiaBrauer/WALRUS/raw/refs/heads/master/demo/data/PEQ_Hupsel.dat # Make data path in walrus.yml absolute client.initialize( os.getcwd() + '/walrus.yml') +client.update() +# model information functions client.get_component_name() 'WALRUS' - -client.update() +client.get_output_var_names() +['ETact', 'Q', 'fGS', 'fQS', 'dV', 'dVeq', 'dG', 'hQ', 'hS', 'w'] +client.get_output_item_count() +10 +client.get_input_var_names() +# kaput, R server returns '' instead of '[]', while reqres::format_json()(list()) does return '[]' +client.get_input_item_count() +0 +# Time functions client.get_current_time() 367417 client.get_time_units() 'hours since 1970-01-01 00:00:00.0 00:00' -client.get_output_var_names() -['ETact', 'Q', 'fGS', 'fQS', 'dV', 'dVeq', 'dG', 'hQ', 'hS', 'w'] -# TODO add to AbstractBmi and WalrusBmi -# client.get_var_location('Q') -client.get_var_type('Q') -numpy.float64 +client.get_time_step() +1 +client.get_end_time() +368904 +client.get_start_time() +367416 +# Variable information functions client.get_var_grid('Q') 0 -client.get_grid_type(0) -'rectilinear' +client.get_var_type('Q') +numpy.float64 +client.get_var_units('Q') +'mm/h' +client.get_var_itemsize('Q') +8 +client.get_var_nbytes('Q') +8 +client.get_var_location('Q') +'node' +# Variable getter and setter functions client.get_value('Q', dest=np.array([.1])) array([0.0044]) -client.get_var_nbytes('Q') -'mm/h' -dest = reserve_values(client, 'Q') -r = client.get_value('Q', dest) -r +client.get_value_at_indices('Q', dest=np.array([0.0]), inds=np.array([0])) +array([0.0044]) # walrus ignores inds and just always returns lumped value +# setter not implemented in walrus +# Model grid functions +client.get_grid_rank(0) +2 +client.get_grid_type(0) +'uniform_rectilinear' +client.getgrid_size(0) +1 +client.get_grid_shape(0, reserve_grid_shape(client, 0)) +array([1, 1]) +client.get_grid_origin(0, reserve_grid_padding(client, 0)) +array([ 6.6544, 52.0613]) +client.get_grid_spacing(0, reserve_grid_padding(client, 0)) +array([0, 0]) +# Other grid function not needed for walrus +# And finally client.finalize() - ``` \ No newline at end of file From 3710b2cd79c6adee9c308689807f5e43513f9e0f Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 14:43:42 +0200 Subject: [PATCH 15/27] Add R6 to deps --- R/remotebmi/DESCRIPTION | 1 + R/remotebmi/man/create_route.Rd | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 R/remotebmi/man/create_route.Rd diff --git a/R/remotebmi/DESCRIPTION b/R/remotebmi/DESCRIPTION index 7b0f550..3fbfd69 100644 --- a/R/remotebmi/DESCRIPTION +++ b/R/remotebmi/DESCRIPTION @@ -15,6 +15,7 @@ Imports: reqres, routr Suggests: + R6 (>= 2.5.1), testthat (>= 3.0.0) Config/testthat/edition: 3 Remotes: diff --git a/R/remotebmi/man/create_route.Rd b/R/remotebmi/man/create_route.Rd new file mode 100644 index 0000000..d6c23c1 --- /dev/null +++ b/R/remotebmi/man/create_route.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/route.R +\name{create_route} +\alias{create_route} +\title{Create a Route for the Given Model} +\usage{ +create_route(model) +} +\arguments{ +\item{model}{The model instance to be used in route handlers. +Must implement the subclass of +\href{https://github.com/eWaterCycle/bmi-r/blob/master/R/abstract-bmi.R}{AbstractBmi} # nolint: line_length_linter.} +} +\value{ +A route object that can be used to interact with the model. +} +\description{ +This function generates a route for the specified model. The route is used to +facilitate communication and interaction with the model. +} +\examples{ +\dontrun{ +model <- SomeModel$new() +route <- create_route(model) +} + +} From 0bdefcd13a53cfcefab830d94115ebbf3354f879 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 15:00:05 +0200 Subject: [PATCH 16/27] Make mock simpler Could not get packages to be installed just for tests --- R/remotebmi/DESCRIPTION | 1 - R/remotebmi/tests/testthat/test-route.R | 30 ++++++++++--------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/R/remotebmi/DESCRIPTION b/R/remotebmi/DESCRIPTION index 3fbfd69..7b0f550 100644 --- a/R/remotebmi/DESCRIPTION +++ b/R/remotebmi/DESCRIPTION @@ -15,7 +15,6 @@ Imports: reqres, routr Suggests: - R6 (>= 2.5.1), testthat (>= 3.0.0) Config/testthat/edition: 3 Remotes: diff --git a/R/remotebmi/tests/testthat/test-route.R b/R/remotebmi/tests/testthat/test-route.R index e6338f7..3ca3fec 100644 --- a/R/remotebmi/tests/testthat/test-route.R +++ b/R/remotebmi/tests/testthat/test-route.R @@ -1,27 +1,21 @@ -library(R6) -library(bmi) - # Poor mans mock bmi_initialize_called_with <<- "" bmi_get_var_units_called_with <<- "" # Mock model object -# Modelled after +# TODO add more functions see # https://github.com/eWaterCycle/grpc4bmi/blob/main/test/fake_models.py -MockedBmi <- R6Class("MockedBmi", - inherit = AbstractBmi, - public = list( - bmi_initialize = function(config_file) { - bmi_initialize_called_with <<- config_file - }, - get_component_name = function() "Mock Component", - get_output_var_names = function() c("var1", "var2"), - get_var_units = function(name) { - bmi_get_var_units_called_with <<- name - return("unit1") - } - ) +# TODO use bmi-r::AbstractModel and R6Class to make proper subclass +mock_model <- list( + bmi_initialize = function(config_file) { + bmi_initialize_called_with <<- config_file + }, + get_component_name = function() "Mock Component", + get_output_var_names = function() c("var1", "var2"), + get_var_units = function(name) { + bmi_get_var_units_called_with <<- name + return("unit1") + } ) -mock_model <- MockedBmi$new() route <- create_route(mock_model) formatter <- reqres::format_json(auto_unbox = TRUE) From 3d7c3f862d6cf230f2f362f2e86f3e36b1b9dfd6 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 15:00:36 +0200 Subject: [PATCH 17/27] Clear workspace from not R files --- .github/workflows/r.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index 15e17b1..2eb3ede 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -25,7 +25,7 @@ jobs: R/remotebmi path: root - name: Move R package to cwd - run: shopt -s dotglob && mv root/R/remotebmi/* . + run: shopt -s dotglob && mv root/R/remotebmi/* . && rm -rf root - name: Setup R uses: r-lib/actions/setup-r@v2 - uses: r-lib/actions/setup-r-dependencies@v2 From e43f8ba0332b3f08e867c578718fffa02bdfb544 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 15:51:08 +0200 Subject: [PATCH 18/27] Add test coverage with codecov --- .github/workflows/r.yml | 43 ++++++++++++++++++++++++++++++++++++----- R/remotebmi/README.md | 2 +- README.md | 1 + codecov.yml | 20 +++++++++++++++++++ 4 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 codecov.yml diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index 2eb3ede..dc14eed 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -14,6 +14,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + id-token: write # This is required for requesting the JWT + jobs: r: runs-on: ubuntu-latest @@ -23,13 +26,43 @@ jobs: with: sparse-checkout: | R/remotebmi - path: root - - name: Move R package to cwd - run: shopt -s dotglob && mv root/R/remotebmi/* . && rm -rf root - name: Setup R uses: r-lib/actions/setup-r@v2 + working-directory: R/remotebmi - uses: r-lib/actions/setup-r-dependencies@v2 with: - extra-packages: any::rcmdcheck - needs: check + extra-packages: any::rcmdcheck, any::covr, any::xml2 + needs: check, coverage + working-directory: R/remotebmi - uses: r-lib/actions/check-r-package@v2 + working-directory: R/remotebmi + - name: Test coverage + working-directory: R/remotebmi + run: | + cov <- covr::package_coverage( + quiet = FALSE, + clean = FALSE, + install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") + ) + covr::to_cobertura(cov) + shell: Rscript {0} + - uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: ${{ github.event_name != 'pull_request' && true || false }} + file: ./R/remotebmi/cobertura.xml + plugin: noop + disable_search: true + use_oidc: true + - name: Show testthat output + if: always() + run: | + ## -------------------------------------------------------------------- + find '${{ runner.temp }}/R/remotebmi/package' -name 'testthat.Rout*' -exec cat '{}' \; || true + shell: bash + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: coverage-test-failures + path: ${{ runner.temp }}/R/remotebmi/package diff --git a/R/remotebmi/README.md b/R/remotebmi/README.md index 245474b..ae46cb8 100644 --- a/R/remotebmi/README.md +++ b/R/remotebmi/README.md @@ -104,4 +104,4 @@ array([0, 0]) # Other grid function not needed for walrus # And finally client.finalize() -``` \ No newline at end of file +``` diff --git a/README.md b/README.md index aa9f9f6..5452660 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Remote BMI +[![Codecov test coverage](https://codecov.io/gh/eWaterCycle/remotebmi/graph/badge.svg)](https://app.codecov.io/gh/eWaterCycle/remotebmi) The [Basic Model Interface (BMI)](https://bmi.readthedocs.io/en/stable/) is a standard interface for models. The interface is available in different languages and a [language agnosting version in SIDL](https://github.com/csdms/bmi/blob/stable/bmi.sidl). diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..dc5f2b0 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,20 @@ +comment: + layout: "header, diff, flags, components" # show component info in the PR comment + +component_management: + default_rules: # default rules that will be inherited by all components + statuses: + - type: project # in this case every component that doens't have a status defined will have a project type one + target: auto + branches: + - "!main" + individual_components: + - component_id: Python + paths: + - python/** + - component_id: Julia + paths: + - RemoteBMI.jl/** + - component_id: R + paths: + - R/remotebmi/** From 7f9465685839cb6e9c71bafb958056a0f133c25d Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 16:02:47 +0200 Subject: [PATCH 19/27] Move wd --- .github/workflows/r.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index dc14eed..8bcdafb 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -22,20 +22,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v4 with: sparse-checkout: | R/remotebmi - name: Setup R uses: r-lib/actions/setup-r@v2 - working-directory: R/remotebmi + with: - uses: r-lib/actions/setup-r-dependencies@v2 with: extra-packages: any::rcmdcheck, any::covr, any::xml2 needs: check, coverage - working-directory: R/remotebmi + working-directory: R/remotebmi - uses: r-lib/actions/check-r-package@v2 - working-directory: R/remotebmi + with: + working-directory: R/remotebmi - name: Test coverage working-directory: R/remotebmi run: | From b484373b411a1cedc47cee0696e426865cf7c83f Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 16:03:43 +0200 Subject: [PATCH 20/27] No with --- .github/workflows/r.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index 8bcdafb..81e76dc 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -28,7 +28,6 @@ jobs: R/remotebmi - name: Setup R uses: r-lib/actions/setup-r@v2 - with: - uses: r-lib/actions/setup-r-dependencies@v2 with: extra-packages: any::rcmdcheck, any::covr, any::xml2 From cff964aacd0e75aceee6ae69abb711922abf9534 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 16:19:00 +0200 Subject: [PATCH 21/27] Codecov needs full git --- .github/workflows/r.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index 81e76dc..4d830fc 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -23,9 +23,6 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - with: - sparse-checkout: | - R/remotebmi - name: Setup R uses: r-lib/actions/setup-r@v2 - uses: r-lib/actions/setup-r-dependencies@v2 From e21874dadf2d110037c9e91657e2c4ab6836edc8 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 14 Oct 2024 13:20:23 +0200 Subject: [PATCH 22/27] More R tests --- R/remotebmi/tests/testthat/test-route.R | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/R/remotebmi/tests/testthat/test-route.R b/R/remotebmi/tests/testthat/test-route.R index 3ca3fec..0b8837e 100644 --- a/R/remotebmi/tests/testthat/test-route.R +++ b/R/remotebmi/tests/testthat/test-route.R @@ -11,6 +11,10 @@ mock_model <- list( }, get_component_name = function() "Mock Component", get_output_var_names = function() c("var1", "var2"), + get_output_item_count = function() 2, + get_input_var_names = function() c(), + get_input_item_count = function() 0, + get_time_units = function() "h", get_var_units = function(name) { bmi_get_var_units_called_with <<- name return("unit1") @@ -19,6 +23,7 @@ mock_model <- list( route <- create_route(mock_model) formatter <- reqres::format_json(auto_unbox = TRUE) +formatter_plain <- reqres::format_json() test_that("/get_component_name", { fake_rook <- fiery::fake_request("/get_component_name") @@ -60,3 +65,40 @@ test_that("/get_var_units", { expect_equal(res$body, formatter(list(units = "unit1"))) expect_equal(bmi_get_var_units_called_with, "Q") }) + +test_that("/get_input_var_names", { + fake_rook <- fiery::fake_request("/get_input_var_names") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + # TODO should return json string `[]` + expect_equal(res$body, NULL) +}) + +test_that("/get_input_item_count", { + fake_rook <- fiery::fake_request("/get_input_item_count") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(0)) +}) + +test_that("/get_output_item_count", { + fake_rook <- fiery::fake_request("/get_output_item_count") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(2)) +}) + +test_that("/get_time_units", { + fake_rook <- fiery::fake_request("/get_time_units") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(list(units = "h"))) +}) \ No newline at end of file From 11277c52ab64daf4924de2374701b339c87d815f Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 14 Oct 2024 13:50:11 +0200 Subject: [PATCH 23/27] Be transparent about copilot --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 5452660..bdbff56 100644 --- a/README.md +++ b/README.md @@ -117,3 +117,9 @@ serve(MyModel::ModelBmi$new(), port=port, host="localhost") Any language that can run a HTTP server and parse/load JSON can be used as a provider. The server should implement the [openapi.yaml specification](openapi.yaml). + +## AI Disclaimer + +The documentation/software code in this repository has been generated and/or refined using +GitHub CoPilot. All AI-output has been verified for correctness, +accuracy and completeness, adapted where needed, and approved by the author. From e39fdda20b2c601500a916e1067a52f02c8e9e6e Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 14 Oct 2024 15:02:45 +0200 Subject: [PATCH 24/27] All the R routes tested --- R/remotebmi/R/route.R | 9 +- R/remotebmi/tests/testthat/test-route.R | 469 +++++++++++++++++++++++- 2 files changed, 463 insertions(+), 15 deletions(-) diff --git a/R/remotebmi/R/route.R b/R/remotebmi/R/route.R index 3337c93..62a088a 100644 --- a/R/remotebmi/R/route.R +++ b/R/remotebmi/R/route.R @@ -163,7 +163,8 @@ create_route <- function(model) { get_var_units <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- list(units = model$get_var_units(last_segment(request$path))) + units <- model$get_var_units(last_segment(request$path)) + response$body <- list(units = units) response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -294,7 +295,7 @@ create_route <- function(model) { response$status <- 200L response$type <- "application/json" response$body <- model$get_grid_node_count(keys$grid) - response$format(json = reqres::format_json()) + response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -302,7 +303,7 @@ create_route <- function(model) { response$status <- 200L response$type <- "application/json" response$body <- model$get_grid_edge_count(keys$grid) - response$format(json = reqres::format_json()) + response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } @@ -310,7 +311,7 @@ create_route <- function(model) { response$status <- 200L response$type <- "application/json" response$body <- model$get_grid_face_count(keys$grid) - response$format(json = reqres::format_json()) + response$format(json = reqres::format_json(auto_unbox = TRUE)) return(FALSE) } diff --git a/R/remotebmi/tests/testthat/test-route.R b/R/remotebmi/tests/testthat/test-route.R index 0b8837e..84902b5 100644 --- a/R/remotebmi/tests/testthat/test-route.R +++ b/R/remotebmi/tests/testthat/test-route.R @@ -1,13 +1,21 @@ # Poor mans mock -bmi_initialize_called_with <<- "" -bmi_get_var_units_called_with <<- "" +method_called_with <- list() # Mock model object # TODO add more functions see # https://github.com/eWaterCycle/grpc4bmi/blob/main/test/fake_models.py # TODO use bmi-r::AbstractModel and R6Class to make proper subclass mock_model <- list( bmi_initialize = function(config_file) { - bmi_initialize_called_with <<- config_file + method_called_with[["bmi_initialize"]] <<- as.character(config_file) + }, + update = function() { + method_called_with[["update"]] <<- TRUE + }, + update_until = function(time) { + method_called_with[["update_until"]] <<- as.numeric(time) + }, + bmi_finalize = function() { + method_called_with[["bmi_finalize"]] <<- TRUE }, get_component_name = function() "Mock Component", get_output_var_names = function() c("var1", "var2"), @@ -15,9 +23,113 @@ mock_model <- list( get_input_var_names = function() c(), get_input_item_count = function() 0, get_time_units = function() "h", + get_time_step = function() 1, + get_current_time = function() 12, + get_start_time = function() 0, + get_end_time = function() 168, + get_var_grid = function(name) { + method_called_with[["get_var_grid"]] <<- as.character(name) + return(1) + }, + get_var_type = function(name) { + method_called_with[["get_var_type"]] <<- as.character(name) + return("float64") + }, + get_var_nbytes = function(name) { + method_called_with[["get_var_nbytes"]] <<- as.character(name) + return(8) + }, + get_var_itemsize = function(name) { + method_called_with[["get_var_itemsize"]] <<- as.character(name) + return(8) + }, + get_var_location = function(name) { + method_called_with[["get_var_location"]] <<- as.character(name) + return("node") + }, get_var_units = function(name) { - bmi_get_var_units_called_with <<- name + method_called_with[["get_var_units"]] <<- as.character(name) return("unit1") + }, + get_value = function(name) { + method_called_with[["get_value"]] <<- list(name = name) + return(4.2) + }, + get_value_at_indices = function(name, indices) { + args <- list(name = name, indices = indices) + method_called_with[["get_value_at_indices"]] <<- args + return(4.2) + }, + set_value = function(name, value) { + method_called_with[["set_value"]] <<- list(name = name, value = value) + }, + set_value_at_indices = function(name, indices, values) { + args <- list(name = name, indices = indices, values = values) + method_called_with[["set_value_at_indices"]] <<- args + }, + get_grid_rank = function(grid_id) { + method_called_with[["get_grid_rank"]] <<- as.character(grid_id) + return(24) + }, + get_grid_type = function(grid_id) { + method_called_with[["get_grid_type"]] <<- as.character(grid_id) + return("uniform_rectilinear") + }, + get_grid_size = function(grid_id) { + method_called_with[["get_grid_size"]] <<- as.character(grid_id) + return(24) + }, + get_grid_x = function(grid_id) { + method_called_with[["get_grid_x"]] <<- as.character(grid_id) + return(c(0.1, 0.2, 0.3, 0.4)) + }, + get_grid_y = function(grid_id) { + method_called_with[["get_grid_y"]] <<- as.character(grid_id) + return(c(1.1, 1.2, 1.3)) + }, + get_grid_z = function(grid_id) { + method_called_with[["get_grid_z"]] <<- as.character(grid_id) + return(c(2.1, 2.2)) + }, + get_grid_shape = function(grid_id) { + method_called_with[["get_grid_shape"]] <<- as.character(grid_id) + return(c(2, 3, 4)) + }, + get_grid_origin = function(grid_id) { + method_called_with[["get_grid_origin"]] <<- as.character(grid_id) + return(c(0.1, 1.1, 2.1)) + }, + get_grid_spacing = function(grid_id) { + method_called_with[["get_grid_spacing"]] <<- as.character(grid_id) + return(c(0.1, 0.2, 0.3)) + }, + get_grid_node_count = function(grid_id) { + method_called_with[["get_grid_node_count"]] <<- as.character(grid_id) + return(6) + }, + get_grid_edge_count = function(grid_id) { + method_called_with[["get_grid_edge_count"]] <<- as.character(grid_id) + return(8) + }, + get_grid_face_count = function(grid_id) { + method_called_with[["get_grid_face_count"]] <<- as.character(grid_id) + return(3) + }, + get_grid_edge_nodes = function(grid_id) { + method_called_with[["get_grid_edge_nodes"]] <<- as.character(grid_id) + return(c(0, 1, 1, 2, 2, 3, 3, 0, 1, 4, 4, 5, 5, 2, 5, 3)) + }, + get_grid_face_edges = function(grid_id) { + method_called_with[["get_grid_face_edges"]] <<- as.character(grid_id) + return(c(0, 1, 2, 3, 4, 5, 6, 1, 6, 7, 2)) + }, + get_grid_face_nodes = function(grid_id) { + method_called_with[["get_grid_face_nodes"]] <<- as.character(grid_id) + return(c(0, 1, 2, 3, 1, 4, 5, 2, 2, 5, 3)) + }, + get_grid_nodes_per_face = function(grid_id) { + method_called_with[["get_grid_nodes_per_face"]] <<- as.character(grid_id) + return(c(4, 4, 3)) } ) @@ -53,17 +165,42 @@ test_that("/initialize", { res <- req$respond() route$dispatch(req) expect_equal(res$status, 201) - expect_equal(bmi_initialize_called_with, "some_config") + expect_equal(method_called_with[["bmi_initialize"]], "some_config") }) -test_that("/get_var_units", { - fake_rook <- fiery::fake_request("/get_var_units/Q") +test_that("/update", { + fake_rook <- fiery::fake_request("/update", + method = "post" + ) req <- reqres::Request$new(fake_rook) res <- req$respond() route$dispatch(req) - expect_equal(res$status, 200) - expect_equal(res$body, formatter(list(units = "unit1"))) - expect_equal(bmi_get_var_units_called_with, "Q") + expect_equal(res$status, 204) + expect_equal(method_called_with[["update"]], TRUE) +}) + +test_that("/update_until", { + fake_rook <- fiery::fake_request("/update_until", + content = "113", + method = "post", + headers = list("Content_Type" = "application/json") + ) + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 204) + expect_equal(method_called_with[["update_until"]], 113) +}) + +test_that("/finalize", { + fake_rook <- fiery::fake_request("/finalize", + method = "delete" + ) + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 204) + expect_equal(method_called_with[["bmi_finalize"]], TRUE) }) test_that("/get_input_var_names", { @@ -101,4 +238,314 @@ test_that("/get_time_units", { route$dispatch(req) expect_equal(res$status, 200) expect_equal(res$body, formatter(list(units = "h"))) -}) \ No newline at end of file +}) + +test_that("/get_time_step", { + fake_rook <- fiery::fake_request("/get_time_step") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(1)) +}) + +test_that("/get_current_time", { + fake_rook <- fiery::fake_request("/get_current_time") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(12)) +}) + +test_that("/get_start_time", { + fake_rook <- fiery::fake_request("/get_start_time") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(0)) +}) + +test_that("/get_end_time", { + fake_rook <- fiery::fake_request("/get_end_time") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(168)) +}) + +test_that("/get_var_units", { + fake_rook <- fiery::fake_request("/get_var_units/Q") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(list(units = "unit1"))) + expect_equal(method_called_with[["get_var_units"]], "Q") +}) + +test_that("/get_var_grid", { + fake_rook <- fiery::fake_request("/get_var_grid/Q") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(1)) + expect_equal(method_called_with[["get_var_grid"]], "Q") +}) + +test_that("/get_var_type", { + fake_rook <- fiery::fake_request("/get_var_type/Q") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(list(type = "double"))) + expect_equal(method_called_with[["get_var_type"]], "Q") +}) + +test_that("/get_var_nbytes", { + fake_rook <- fiery::fake_request("/get_var_nbytes/Q") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(8)) + expect_equal(method_called_with[["get_var_nbytes"]], "Q") +}) + +test_that("/get_var_itemsize", { + fake_rook <- fiery::fake_request("/get_var_itemsize/Q") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(8)) + expect_equal(method_called_with[["get_var_itemsize"]], "Q") +}) + +test_that("/get_var_location", { + fake_rook <- fiery::fake_request("/get_var_location/Q") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(list(location = "node"))) + expect_equal(method_called_with[["get_var_location"]], "Q") +}) + +test_that("/get_value", { + fake_rook <- fiery::fake_request("/get_value/Q") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(list(4.2))) + expect_equal(method_called_with[["get_value"]], list(name = "Q")) +}) + +test_that("/get_value_at_indices", { + fake_rook <- fiery::fake_request("/get_value_at_indices/Q", + content = "[1, 2, 3]", + method = "post", + headers = list("Content_Type" = "application/json") + ) + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(list(4.2))) + expected <- list(name = "Q", indices = c(1, 2, 3)) + expect_equal(method_called_with[["get_value_at_indices"]], expected) +}) + +test_that("/set_value", { + fake_rook <- fiery::fake_request("/set_value/Q", + content = "[4.2]", + method = "post", + headers = list("Content_Type" = "application/json") + ) + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 204) + expected <- list(name = "Q", value = c(4.2)) + expect_equal(method_called_with[["set_value"]], expected) +}) + +test_that("set_value_at_indices", { + fake_rook <- fiery::fake_request("/set_value_at_indices/Q", + content = '{"indices": [1, 2, 3], "values": [1.1, 2.2, 3.3]}', + method = "post", + headers = list("Content_Type" = "application/json") + ) + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 204) + expected <- list(name = "Q", indices = c(1, 2, 3), values = c(1.1, 2.2, 3.3)) + expect_equal(method_called_with[["set_value_at_indices"]], expected) +}) + +test_that("/get_grid_rank", { + fake_rook <- fiery::fake_request("/get_grid_rank/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(24)) + expect_equal(method_called_with[["get_grid_rank"]], "1") +}) + +test_that("/get_grid_type", { + fake_rook <- fiery::fake_request("/get_grid_type/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(list(type = "uniform_rectilinear"))) + expect_equal(method_called_with[["get_grid_type"]], "1") +}) + +test_that("/get_grid_size", { + fake_rook <- fiery::fake_request("/get_grid_size/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(24)) + expect_equal(method_called_with[["get_grid_size"]], "1") +}) + +test_that("/get_grid_x", { + fake_rook <- fiery::fake_request("/get_grid_x/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c(0.1, 0.2, 0.3, 0.4))) + expect_equal(method_called_with[["get_grid_x"]], "1") +}) + +test_that("/get_grid_y", { + fake_rook <- fiery::fake_request("/get_grid_y/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c(1.1, 1.2, 1.3))) + expect_equal(method_called_with[["get_grid_y"]], "1") +}) + +test_that("/get_grid_z", { + fake_rook <- fiery::fake_request("/get_grid_z/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c(2.1, 2.2))) + expect_equal(method_called_with[["get_grid_z"]], "1") +}) + +test_that("/get_grid_shape", { + fake_rook <- fiery::fake_request("/get_grid_shape/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c(2, 3, 4))) + expect_equal(method_called_with[["get_grid_shape"]], "1") +}) + +test_that("/get_grid_origin", { + fake_rook <- fiery::fake_request("/get_grid_origin/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c(0.1, 1.1, 2.1))) + expect_equal(method_called_with[["get_grid_origin"]], "1") +}) + +test_that("/get_grid_spacing", { + fake_rook <- fiery::fake_request("/get_grid_spacing/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c(0.1, 0.2, 0.3))) + expect_equal(method_called_with[["get_grid_spacing"]], "1") +}) + +test_that("/get_grid_node_count", { + fake_rook <- fiery::fake_request("/get_grid_node_count/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(6)) + expect_equal(method_called_with[["get_grid_node_count"]], "1") +}) + +test_that("/get_grid_edge_count", { + fake_rook <- fiery::fake_request("/get_grid_edge_count/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(8)) + expect_equal(method_called_with[["get_grid_edge_count"]], "1") +}) + +test_that("/get_grid_face_count", { + fake_rook <- fiery::fake_request("/get_grid_face_count/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(3)) + expect_equal(method_called_with[["get_grid_face_count"]], "1") +}) + +test_that("/get_grid_edge_nodes", { + fake_rook <- fiery::fake_request("/get_grid_edge_nodes/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + en <- c(0, 1, 1, 2, 2, 3, 3, 0, 1, 4, 4, 5, 5, 2, 5, 3) + expect_equal(res$body, formatter(en)) + expect_equal(method_called_with[["get_grid_edge_nodes"]], "1") +}) + +test_that("/get_grid_face_edges", { + fake_rook <- fiery::fake_request("/get_grid_face_edges/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c(0, 1, 2, 3, 4, 5, 6, 1, 6, 7, 2))) + expect_equal(method_called_with[["get_grid_face_edges"]], "1") +}) + +test_that("/get_grid_face_nodes", { + fake_rook <- fiery::fake_request("/get_grid_face_nodes/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c(0, 1, 2, 3, 1, 4, 5, 2, 2, 5, 3))) + expect_equal(method_called_with[["get_grid_face_nodes"]], "1") +}) + +test_that("/get_grid_nodes_per_face", { + fake_rook <- fiery::fake_request("/get_grid_nodes_per_face/1") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 200) + expect_equal(res$body, formatter(c(4, 4, 3))) + expect_equal(method_called_with[["get_grid_nodes_per_face"]], "1") +}) From f6868ee0a18ea93fb264a54e0db6323b4ceffed8 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 14 Oct 2024 15:06:28 +0200 Subject: [PATCH 25/27] Also test fallback --- R/remotebmi/tests/testthat/test-route.R | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/R/remotebmi/tests/testthat/test-route.R b/R/remotebmi/tests/testthat/test-route.R index 84902b5..c456747 100644 --- a/R/remotebmi/tests/testthat/test-route.R +++ b/R/remotebmi/tests/testthat/test-route.R @@ -135,7 +135,6 @@ mock_model <- list( route <- create_route(mock_model) formatter <- reqres::format_json(auto_unbox = TRUE) -formatter_plain <- reqres::format_json() test_that("/get_component_name", { fake_rook <- fiery::fake_request("/get_component_name") @@ -549,3 +548,12 @@ test_that("/get_grid_nodes_per_face", { expect_equal(res$body, formatter(c(4, 4, 3))) expect_equal(method_called_with[["get_grid_nodes_per_face"]], "1") }) + +test_that("fallback route", { + fake_rook <- fiery::fake_request("/random_url_is_not_found") + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 404) + expect_equal(res$body, "Not found") +}) \ No newline at end of file From 27a583fb875fd4d040879ea73aa08db0ce51591a Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Tue, 15 Oct 2024 10:25:33 +0200 Subject: [PATCH 26/27] Make get_input_var_names return [] + force arrays to be non-empty + some request validation --- R/remotebmi/R/route.R | 38 +++++++++++++++--- R/remotebmi/tests/testthat/test-route.R | 51 ++++++++++++++++++++++++- openapi.yaml | 7 ++++ 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/R/remotebmi/R/route.R b/R/remotebmi/R/route.R index 62a088a..641ce80 100644 --- a/R/remotebmi/R/route.R +++ b/R/remotebmi/R/route.R @@ -63,8 +63,13 @@ create_route <- function(model) { get_output_var_names <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$get_output_var_names() - response$format(json = reqres::format_json()) + names <- model$get_output_var_names() + if (is.null(names)) { + response$body <- "[]" + } else { + response$body <- names + response$format(json = reqres::format_json()) + } return(FALSE) } @@ -79,8 +84,13 @@ create_route <- function(model) { get_input_var_names <- function(request, response, keys, ...) { response$status <- 200L response$type <- "application/json" - response$body <- model$get_input_var_names() - response$format(json = reqres::format_json()) + names <- model$get_input_var_names() + if (is.null(names)) { + response$body <- "[]" + } else { + response$body <- names + response$format(json = reqres::format_json()) + } return(FALSE) } @@ -199,7 +209,25 @@ create_route <- function(model) { response$status <- 200L response$type <- "application/json" name <- last_segment(request$path) - response$body <- model$get_value_at_indices(name, request$body) + indices <- request$body + if (!is.integer(indices) && !is.list(indices)) { + response$status <- 400L + response$body <- list(title = "Request body must be an array") + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } else if (length(indices) == 0) { + response$status <- 400L + response$body <- list(title = "Request body must be a non-empty list") + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } else if (any(indices < 0)) { + response$status <- 400L + title <- "Each request body item must be a non-negative integer" + response$body <- list(title = title) + response$format(json = reqres::format_json(auto_unbox = TRUE)) + return(FALSE) + } + response$body <- model$get_value_at_indices(name, indices) response$format(json = reqres::format_json()) return(FALSE) } diff --git a/R/remotebmi/tests/testthat/test-route.R b/R/remotebmi/tests/testthat/test-route.R index c456747..25e8729 100644 --- a/R/remotebmi/tests/testthat/test-route.R +++ b/R/remotebmi/tests/testthat/test-route.R @@ -208,8 +208,7 @@ test_that("/get_input_var_names", { res <- req$respond() route$dispatch(req) expect_equal(res$status, 200) - # TODO should return json string `[]` - expect_equal(res$body, NULL) + expect_equal(res$body, "[]") }) test_that("/get_input_item_count", { @@ -360,6 +359,54 @@ test_that("/get_value_at_indices", { expect_equal(method_called_with[["get_value_at_indices"]], expected) }) +test_that("/get_value_at_indices, given string", { + fake_rook <- fiery::fake_request("/get_value_at_indices/Q", + content = "foobar", + method = "post", + headers = list("Content_Type" = "application/json") + ) + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 400) + expected <- list( + title = "Request body must be an array" + ) + expect_equal(res$body, formatter(expected)) +}) + +test_that("/get_value_at_indices, given empty list", { + fake_rook <- fiery::fake_request("/get_value_at_indices/Q", + content = "[]", + method = "post", + headers = list("Content_Type" = "application/json") + ) + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 400) + expected <- list( + title = "Request body must be a non-empty list" + ) + expect_equal(res$body, formatter(expected)) +}) + +test_that("/get_value_at_indices, given negative index", { + fake_rook <- fiery::fake_request("/get_value_at_indices/Q", + content = "[-1]", + method = "post", + headers = list("Content_Type" = "application/json") + ) + req <- reqres::Request$new(fake_rook) + res <- req$respond() + route$dispatch(req) + expect_equal(res$status, 400) + expected <- list( + title = "Each request body item must be a non-negative integer" + ) + expect_equal(res$body, formatter(expected)) +}) + test_that("/set_value", { fake_rook <- fiery::fake_request("/set_value/Q", content = "[4.2]", diff --git a/openapi.yaml b/openapi.yaml index 455c63a..7ffa01a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1138,11 +1138,13 @@ components: format: int64 description: Values of variable are indexed 0-based. minimum: 0 + minItems: 1 Doubles: type: array items: type: number format: double + minItems: 1 GetGridRankResponse: type: integer format: int32 @@ -1186,10 +1188,12 @@ components: type: array items: type: number + minItems: 1 GetValueResponse: type: array items: type: number + minItems: 1 GetVarGridResponse: type: integer format: int32 @@ -1250,10 +1254,12 @@ components: description: Values of variable are indexed 0-based. format: int64 minimum: 0 + minItems: 1 values: type: array items: type: number + minItems: 1 required: - indices - values @@ -1262,6 +1268,7 @@ components: type: array items: type: number + minItems: 1 Grid: type: integer format: int32 From dffcd83a3dbb8f9b3fb6232c49dc15b0202025b9 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Thu, 17 Oct 2024 09:59:20 +0200 Subject: [PATCH 27/27] Move AI disclaimer to contributing guide --- CONTRIBUTING.md | 6 ++++++ README.md | 6 ------ python/pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c532b2..a7e515f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,11 @@ # Contributing +## AI Assistance + +The documentation/software code in this repository can and has been partly generated and/or refined using +GitHub CoPilot. All AI-output has been verified for correctness, +accuracy and completeness, adapted where needed, and approved by the author. + ## Add another language A directory of the name of the language should be created in the root of the repository. diff --git a/README.md b/README.md index bdbff56..5452660 100644 --- a/README.md +++ b/README.md @@ -117,9 +117,3 @@ serve(MyModel::ModelBmi$new(), port=port, host="localhost") Any language that can run a HTTP server and parse/load JSON can be used as a provider. The server should implement the [openapi.yaml specification](openapi.yaml). - -## AI Disclaimer - -The documentation/software code in this repository has been generated and/or refined using -GitHub CoPilot. All AI-output has been verified for correctness, -accuracy and completeness, adapted where needed, and approved by the author. diff --git a/python/pyproject.toml b/python/pyproject.toml index 1e02062..4a7b9b0 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "bmipy", "numpy", # client - "httpx", + "httpx>=0.27.2", # server "connexion", "uvicorn",