From 4d09603c9288ae15862649c9d961ddac43d4f664 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Tue, 24 Sep 2024 16:07:48 +0200 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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 5780d801b375be09e97818cc776c1b7e00df88cf Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 7 Oct 2024 16:22:50 +0200 Subject: [PATCH 22/30] Added some tests for python --- python/tests/__init__.py | 0 python/tests/fake_models.py | 420 +++++++++++++++++++++++++++++++++++ python/tests/test_reserve.py | 11 + 3 files changed, 431 insertions(+) create mode 100644 python/tests/__init__.py create mode 100644 python/tests/fake_models.py create mode 100644 python/tests/test_reserve.py diff --git a/python/tests/__init__.py b/python/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/fake_models.py b/python/tests/fake_models.py new file mode 100644 index 0000000..90eb24b --- /dev/null +++ b/python/tests/fake_models.py @@ -0,0 +1,420 @@ +from typing import Tuple + +import numpy +import numpy as np +from bmipy import Bmi + + +class SomeException(Exception): + pass + + +class FailingModel(Bmi): + def __init__(self, exc): + self.exc = exc + + def initialize(self, filename): + raise self.exc + + def update(self): + raise self.exc + + def update_until(self, time: float) -> None: + raise self.exc + + def finalize(self): + raise self.exc + + def get_component_name(self): + raise self.exc + + def get_input_item_count(self) -> int: + raise self.exc + + def get_output_item_count(self) -> int: + raise self.exc + + def get_input_var_names(self): + raise self.exc + + def get_output_var_names(self): + raise self.exc + + def get_start_time(self): + raise self.exc + + def get_current_time(self): + raise self.exc + + def get_end_time(self): + raise self.exc + + def get_time_step(self): + raise self.exc + + def get_time_units(self): + raise self.exc + + def get_var_type(self, name): + raise self.exc + + def get_var_units(self, name): + raise self.exc + + def get_var_itemsize(self, name): + raise self.exc + + def get_var_nbytes(self, name): + raise self.exc + + def get_var_grid(self, name): + raise self.exc + + def get_value(self, name, dest): + raise self.exc + + def get_value_ptr(self, name): + raise self.exc + + def get_value_at_indices(self, name, dest, inds): + raise self.exc + + def set_value(self, name, src): + raise self.exc + + def set_value_at_indices(self, name, inds, src): + raise self.exc + + def get_grid_shape(self, grid, shape): + raise self.exc + + def get_grid_x(self, grid, x): + raise self.exc + + def get_grid_y(self, grid, y): + raise self.exc + + def get_grid_z(self, grid, z): + raise self.exc + + def get_grid_spacing(self, grid, spacing): + raise self.exc + + def get_grid_origin(self, grid, origin): + raise self.exc + + def get_grid_rank(self, grid): + raise self.exc + + def get_grid_size(self, grid): + raise self.exc + + def get_grid_type(self, grid): + raise self.exc + + def get_var_location(self, name: str) -> str: + raise self.exc + + def get_grid_node_count(self, grid: int) -> int: + raise self.exc + + def get_grid_edge_count(self, grid: int) -> int: + raise self.exc + + def get_grid_face_count(self, grid: int) -> int: + raise self.exc + + def get_grid_edge_nodes(self, grid: int, edge_nodes: np.ndarray) -> np.ndarray: + raise self.exc + + def get_grid_face_nodes(self, grid: int, face_nodes: np.ndarray) -> np.ndarray: + raise self.exc + + def get_grid_nodes_per_face(self, grid: int, nodes_per_face: np.ndarray) -> np.ndarray: + raise self.exc + + def get_grid_face_edges(self, grid: int, face_edges: np.ndarray) -> np.ndarray: + raise self.exc + + +class GridModel(FailingModel): + def __init__(self): + super(GridModel, self).__init__(SomeException('not used')) + + def initialize(self, filename): + pass + + def get_output_var_names(self) -> Tuple[str]: + return 'plate_surface__temperature', + + def get_var_grid(self, name): + return 0 + + +class UniRectGridModel(GridModel): + def get_grid_type(self, grid): + return 'uniform_rectilinear' + + def get_grid_rank(self, grid): + return 3 + + def get_grid_size(self, grid): + return 24 + + def get_grid_shape(self, grid: int, shape: np.ndarray) -> np.ndarray: + numpy.copyto(src=[2, 3, 4], dst=shape) + return shape + + def get_grid_origin(self, grid, dest): + numpy.copyto(src=[0.1, 1.1, 2.1], dst=dest) + return dest + + def get_grid_spacing(self, grid, dest): + numpy.copyto(src=[0.1, 0.2, 0.3], dst=dest) + return dest + + +class Rect3DGridModel(GridModel): + def get_grid_type(self, grid): + return 'rectilinear' + + def get_grid_size(self, grid): + return 24 + + def get_grid_rank(self, grid: int) -> int: + return 3 + + def get_grid_shape(self, grid: int, shape: np.ndarray) -> np.ndarray: + numpy.copyto(src=[2, 3, 4], dst=shape) + return shape + + def get_grid_x(self, grid: int, x: np.ndarray) -> np.ndarray: + numpy.copyto(src=[0.1, 0.2, 0.3, 0.4], dst=x) + return x + + def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: + numpy.copyto(src=[1.1, 1.2, 1.3], dst=y) + return y + + def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: + numpy.copyto(src=[2.1, 2.2], dst=z) + return z + + +class Rect2DGridModel(Rect3DGridModel): + def get_grid_size(self, grid): + return 12 # 4*3 + + def get_grid_rank(self, grid: int) -> int: + return 2 + + def get_grid_shape(self, grid: int, shape: np.ndarray) -> np.ndarray: + numpy.copyto(src=[3, 4], dst=shape) + return shape + + def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: + raise NotImplementedError('Do not know what z is') + + +class Structured3DQuadrilateralsGridModel(GridModel): + # Grid shape: + # 0 + # / \ + # / \ + # 3 1 + # \ / + # \ / + # 2 + # + def get_grid_type(self, grid): + return 'structured_quadrilateral' + + def get_grid_rank(self, grid: int) -> int: + return 3 + + def get_grid_size(self, grid): + return 4 + + def get_grid_shape(self, grid, shape): + numpy.copyto(src=[1, 2, 2], dst=shape) + return shape + + def get_grid_x(self, grid, x): + numpy.copyto(src=[1.1, 0.1, 1.1, 2.1], dst=x) + return x + + def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: + numpy.copyto(src=[2.2, 1.2, 0.2, 2.2], dst=y) + return y + + def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: + numpy.copyto(src=[1.1, 2.2, 3.3, 4.4], dst=z) + return z + + +class Structured2DQuadrilateralsGridModel(GridModel): + # Grid shape: + # 0 + # / \ + # / \ + # 3 1 + # \ / + # \ / + # 2 + # + def get_grid_type(self, grid): + return 'structured_quadrilateral' + + def get_grid_rank(self, grid: int) -> int: + return 2 + + def get_grid_size(self, grid): + return 4 + + def get_grid_shape(self, grid, shape): + numpy.copyto(src=[2, 2], dst=shape) + return shape + + def get_grid_x(self, grid, x): + numpy.copyto(src=[1.1, 0.1, 1.1, 2.1], dst=x) + return x + + def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: + numpy.copyto(src=[2.2, 1.2, 0.2, 2.2], dst=y) + return y + + def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: + raise NotImplementedError('Do not know what z is') + + +class UnstructuredGridBmiModel(GridModel): + # Uses grid example at https://bmi.readthedocs.io/en/latest/model_grids.html#unstructured-grids + # Grid shape: + # 3-----\ + # / \ \ + # 0 \ \---5 + # \ 2--/ / + # \ / / + # \ / / + # 1---\ / + # 4 + def get_grid_type(self, grid): + return 'unstructured' + + def get_grid_shape(self, grid, dest): + raise NotImplementedError('Do not know what shape is') + + def get_grid_size(self, grid): + return 6 + + def get_grid_rank(self, grid: int) -> int: + return 2 + + def get_grid_node_count(self, grid: int) -> int: + return 6 + + def get_grid_edge_count(self, grid: int) -> int: + return 8 + + def get_grid_face_count(self, grid: int) -> int: + return 3 + + def get_grid_edge_nodes(self, grid: int, edge_nodes: np.ndarray) -> np.ndarray: + numpy.copyto(src=(0, 1, 1, 2, 2, 3, 3, 0, 1, 4, 4, 5, 5, 2, 5, 3), dst=edge_nodes) + return edge_nodes + + def get_grid_face_nodes(self, grid: int, face_nodes: np.ndarray) -> np.ndarray: + numpy.copyto(src=(0, 1, 2, 3, 1, 4, 5, 2, 2, 5, 3), dst=face_nodes) + return face_nodes + + def get_grid_face_edges(self, grid: int, face_edges: np.ndarray) -> np.ndarray: + numpy.copyto(src=(0, 1, 2, 3, 4, 5, 6, 1, 6, 7, 2), dst=face_edges) + return face_edges + + def get_grid_nodes_per_face(self, grid: int, nodes_per_face: np.ndarray) -> np.ndarray: + numpy.copyto(src=(4, 4, 3), dst=nodes_per_face) + return nodes_per_face + + def get_grid_x(self, grid: int, x: np.ndarray) -> np.ndarray: + numpy.copyto(src=[0., 1., 2., 1., 3., 4.], dst=x) + return x + + def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: + numpy.copyto(src=[3., 1., 2., 4., 0., 3.], dst=y) + return y + + def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: + raise NotImplementedError('Do not know what z is') + + +class DTypeModel(GridModel): + def __init__(self): + super().__init__() + self.dtype = numpy.dtype('float32') + self.value = numpy.array((1.1, 2.2, 3.3), dtype=self.dtype) + + def get_var_type(self, name): + return str(self.dtype) + + def get_var_itemsize(self, name): + return self.dtype.itemsize + + def get_var_nbytes(self, name): + return self.dtype.itemsize * self.value.size + + def get_value(self, name, dest): + numpy.copyto(src=self.value, dst=dest) + return dest + + def get_value_at_indices(self, name, dest, inds): + numpy.copyto(src=self.value[inds], dst=dest) + return dest + + def set_value(self, name, src): + self.value[:] = src + + def set_value_at_indices(self, name, inds, src): + self.value[inds] = src + + +class Float32Model(DTypeModel): + pass + + +class Int32Model(DTypeModel): + def __init__(self): + super().__init__() + self.dtype = numpy.dtype('int32') + self.value = numpy.array((12, 24, 36), dtype=self.dtype) + + +class BooleanModel(DTypeModel): + def __init__(self): + super().__init__() + self.dtype = numpy.dtype('bool') + self.value = numpy.array((True, False, True), dtype=self.dtype) + + +class HugeModel(DTypeModel): + """Model which has value which does not fit in single message body + + Can be run from command line with + + ..code-block:: bash + + run-bmi-server --path $PWD/test --name fake_models.HugeModel --port 55555 --debug + """ + def __init__(self): + super().__init__() + self.dtype = numpy.dtype('float64') + # Create value which is bigger than 4Mb + dimension = (3 * 4_000_000) // self.dtype.itemsize + 1000 + self.value = numpy.ones((dimension,), dtype=self.dtype) + +class WithItemSizeZeroAndVarTypeFloat32Model(Float32Model): + def get_var_itemsize(self, name): + return 0 + +class WithItemSizeZeroAndUnknownVarType(WithItemSizeZeroAndVarTypeFloat32Model): + def get_var_type(self, name): + return 'real' \ No newline at end of file diff --git a/python/tests/test_reserve.py b/python/tests/test_reserve.py new file mode 100644 index 0000000..f2182d0 --- /dev/null +++ b/python/tests/test_reserve.py @@ -0,0 +1,11 @@ +import numpy as np + +from remotebmi.reserve import reserve_values +from tests.fake_models import Float32Model + + +def test_reserve_values(): + model = Float32Model() + result = reserve_values(model, "somevar") + assert result.dtype == np.float32 + assert result.size == 3 From e21874dadf2d110037c9e91657e2c4ab6836edc8 Mon Sep 17 00:00:00 2001 From: Stefan Verhoeven Date: Mon, 14 Oct 2024 13:20:23 +0200 Subject: [PATCH 23/30] 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 24/30] 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 25/30] 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 26/30] 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 27/30] 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 28/30] 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", From da6f8559b5ed4c334eb8ce140d4dcf7230812048 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Thu, 7 Nov 2024 16:04:17 +0100 Subject: [PATCH 29/30] In ci add ruff format + test job --- .github/workflows/python.yml | 28 ++++++- python/pyproject.toml | 6 ++ python/remotebmi/client/apptainer.py | 15 ++-- python/remotebmi/client/docker.py | 4 +- python/remotebmi/server/api.py | 4 +- python/remotebmi/server/build.py | 8 +- python/tests/fake_models.py | 111 +++++++++++++++------------ 7 files changed, 108 insertions(+), 68 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 60de449..1b1a754 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -5,19 +5,25 @@ on: paths: - "openapi.yaml" - "python/remotebmi/**" + - "python/tests/**" - .github/workflows/python.yml pull_request_target: paths: - "openapi.yaml" - "python/remotebmi/**" + - "python/tests/**" - .github/workflows/python.yml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +defaults: + run: + working-directory: python + jobs: - python: + lint-format: runs-on: ubuntu-latest steps: @@ -30,5 +36,21 @@ jobs: cache: "pip" - name: Install ruff run: pip install ruff - - name: Run ruff - run: ruff check python/remotebmi \ No newline at end of file + - name: Run ruff check + run: ruff check + - name: Run ruff format + run: ruff format --check + test: + 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 dependencies + run: pip install -e .[dev] + - name: Run tests + run: pytest tests \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml index 1e02062..d0a25f3 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -18,6 +18,12 @@ dependencies = [ "docker", ] +[project.optional-dependencies] +dev = [ + "ruff", + "pytest" +] + [project.scripts] run-bmi-server='remotebmi.server:main' diff --git a/python/remotebmi/client/apptainer.py b/python/remotebmi/client/apptainer.py index 813e49a..983ea97 100644 --- a/python/remotebmi/client/apptainer.py +++ b/python/remotebmi/client/apptainer.py @@ -37,9 +37,7 @@ def __init__( self.work_dir = abspath(work_dir) if self.work_dir in {abspath(d) for d in input_dirs}: msg = "Found work_dir equal to one of the input directories. Please drop that input dir." - raise ValueError( - msg - ) + raise ValueError(msg) if not os.path.isdir(self.work_dir): # noqa: PTH112 raise NotADirectoryError(self.work_dir) args += ["--bind", f"{self.work_dir}:{self.work_dir}:rw"] @@ -48,7 +46,7 @@ def __init__( args.append(image) logging.info(f"Running {image} apptainer container on port {port}") if capture_logs: - self.logfile = SpooledTemporaryFile( + self.logfile = SpooledTemporaryFile( # noqa: SIM115 - file is closed in __del__ max_size=2**16, # keep until 65Kb in memory if bigger write to disk prefix="grpc4bmi-apptainer-log", mode="w+t", @@ -58,12 +56,17 @@ def __init__( else: stdout = subprocess.DEVNULL self.container = subprocess.Popen( # noqa: S603 - args, preexec_fn=os.setsid, stderr=subprocess.STDOUT, stdout=stdout # noqa: PLW1509 if absent leaves zombie processes behind + args, + preexec_fn=os.setsid, + stderr=subprocess.STDOUT, + stdout=stdout, # noqa: PLW1509 if absent leaves zombie processes behind ) time.sleep(delay) returncode = self.container.poll() if returncode is not None: - msg = f"apptainer container {image} prematurely exited with code {returncode}" + msg = ( + f"apptainer container {image} prematurely exited with code {returncode}" + ) raise DeadContainerError( msg, returncode, diff --git a/python/remotebmi/client/docker.py b/python/remotebmi/client/docker.py index 4b49696..7a7e761 100644 --- a/python/remotebmi/client/docker.py +++ b/python/remotebmi/client/docker.py @@ -40,9 +40,7 @@ def __init__( self.work_dir = abspath(work_dir) if self.work_dir in volumes: msg = "Found work_dir equal to one of the input directories. Please drop that input dir." - raise ValueError( - msg - ) + raise ValueError(msg) if not os.path.isdir(self.work_dir): # noqa: PTH112 raise NotADirectoryError(self.work_dir) volumes[self.work_dir] = {"bind": self.work_dir, "mode": "rw"} diff --git a/python/remotebmi/server/api.py b/python/remotebmi/server/api.py index aa61e06..3ebe349 100644 --- a/python/remotebmi/server/api.py +++ b/python/remotebmi/server/api.py @@ -62,7 +62,7 @@ def get_var_grid(name: str): def get_var_type(name: str): - return {"type": model().get_var_type(name) } + return {"type": model().get_var_type(name)} def get_var_units(name: str): @@ -85,8 +85,10 @@ def get_value(name: str): items = reserve_values(model(), name) return model().get_value(name, items) + # TODO correct typings + def get_value_at_indices(name: str, indices: list): items = reserve_values_at_indices(model(), name, indices) return model().get_value_at_indices(name, indices, items) diff --git a/python/remotebmi/server/build.py b/python/remotebmi/server/build.py index 7a3170c..2eb4384 100644 --- a/python/remotebmi/server/build.py +++ b/python/remotebmi/server/build.py @@ -29,9 +29,7 @@ def from_env() -> Bmi: "Missing module name: module could not be derived from environment " f"variable {ENV_BMI_MODULE}" ) - raise ValueError( - msg - ) + raise ValueError(msg) class_name = os.environ.get(ENV_BMI_CLASS, "") if not class_name: @@ -39,9 +37,7 @@ def from_env() -> Bmi: "Missing bmi implementation: class could not be derived from environment" f"variable {ENV_BMI_CLASS}" ) - raise ValueError( - msg - ) + raise ValueError(msg) return build(module_name, class_name, path) diff --git a/python/tests/fake_models.py b/python/tests/fake_models.py index 90eb24b..86cd211 100644 --- a/python/tests/fake_models.py +++ b/python/tests/fake_models.py @@ -1,11 +1,13 @@ -from typing import Tuple +"""Fake models for testing purposes. + +Copied from https://github.com/eWaterCycle/grpc4bmi/blob/main/test/fake_models.py +""" -import numpy import numpy as np from bmipy import Bmi -class SomeException(Exception): +class SomeError(Exception): pass @@ -130,7 +132,9 @@ def get_grid_edge_nodes(self, grid: int, edge_nodes: np.ndarray) -> np.ndarray: def get_grid_face_nodes(self, grid: int, face_nodes: np.ndarray) -> np.ndarray: raise self.exc - def get_grid_nodes_per_face(self, grid: int, nodes_per_face: np.ndarray) -> np.ndarray: + def get_grid_nodes_per_face( + self, grid: int, nodes_per_face: np.ndarray + ) -> np.ndarray: raise self.exc def get_grid_face_edges(self, grid: int, face_edges: np.ndarray) -> np.ndarray: @@ -139,13 +143,13 @@ def get_grid_face_edges(self, grid: int, face_edges: np.ndarray) -> np.ndarray: class GridModel(FailingModel): def __init__(self): - super(GridModel, self).__init__(SomeException('not used')) + super().__init__(SomeError("not used")) def initialize(self, filename): pass - def get_output_var_names(self) -> Tuple[str]: - return 'plate_surface__temperature', + def get_output_var_names(self) -> tuple[str]: + return ("plate_surface__temperature",) def get_var_grid(self, name): return 0 @@ -153,7 +157,7 @@ def get_var_grid(self, name): class UniRectGridModel(GridModel): def get_grid_type(self, grid): - return 'uniform_rectilinear' + return "uniform_rectilinear" def get_grid_rank(self, grid): return 3 @@ -162,21 +166,21 @@ def get_grid_size(self, grid): return 24 def get_grid_shape(self, grid: int, shape: np.ndarray) -> np.ndarray: - numpy.copyto(src=[2, 3, 4], dst=shape) + np.copyto(src=[2, 3, 4], dst=shape) return shape def get_grid_origin(self, grid, dest): - numpy.copyto(src=[0.1, 1.1, 2.1], dst=dest) + np.copyto(src=[0.1, 1.1, 2.1], dst=dest) return dest def get_grid_spacing(self, grid, dest): - numpy.copyto(src=[0.1, 0.2, 0.3], dst=dest) + np.copyto(src=[0.1, 0.2, 0.3], dst=dest) return dest class Rect3DGridModel(GridModel): def get_grid_type(self, grid): - return 'rectilinear' + return "rectilinear" def get_grid_size(self, grid): return 24 @@ -185,19 +189,19 @@ def get_grid_rank(self, grid: int) -> int: return 3 def get_grid_shape(self, grid: int, shape: np.ndarray) -> np.ndarray: - numpy.copyto(src=[2, 3, 4], dst=shape) + np.copyto(src=[2, 3, 4], dst=shape) return shape def get_grid_x(self, grid: int, x: np.ndarray) -> np.ndarray: - numpy.copyto(src=[0.1, 0.2, 0.3, 0.4], dst=x) + np.copyto(src=[0.1, 0.2, 0.3, 0.4], dst=x) return x def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: - numpy.copyto(src=[1.1, 1.2, 1.3], dst=y) + np.copyto(src=[1.1, 1.2, 1.3], dst=y) return y def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: - numpy.copyto(src=[2.1, 2.2], dst=z) + np.copyto(src=[2.1, 2.2], dst=z) return z @@ -209,11 +213,12 @@ def get_grid_rank(self, grid: int) -> int: return 2 def get_grid_shape(self, grid: int, shape: np.ndarray) -> np.ndarray: - numpy.copyto(src=[3, 4], dst=shape) + np.copyto(src=[3, 4], dst=shape) return shape def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: - raise NotImplementedError('Do not know what z is') + msg = "Do not know what z is" + raise NotImplementedError(msg) class Structured3DQuadrilateralsGridModel(GridModel): @@ -227,7 +232,7 @@ class Structured3DQuadrilateralsGridModel(GridModel): # 2 # def get_grid_type(self, grid): - return 'structured_quadrilateral' + return "structured_quadrilateral" def get_grid_rank(self, grid: int) -> int: return 3 @@ -236,19 +241,19 @@ def get_grid_size(self, grid): return 4 def get_grid_shape(self, grid, shape): - numpy.copyto(src=[1, 2, 2], dst=shape) + np.copyto(src=[1, 2, 2], dst=shape) return shape def get_grid_x(self, grid, x): - numpy.copyto(src=[1.1, 0.1, 1.1, 2.1], dst=x) + np.copyto(src=[1.1, 0.1, 1.1, 2.1], dst=x) return x def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: - numpy.copyto(src=[2.2, 1.2, 0.2, 2.2], dst=y) + np.copyto(src=[2.2, 1.2, 0.2, 2.2], dst=y) return y def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: - numpy.copyto(src=[1.1, 2.2, 3.3, 4.4], dst=z) + np.copyto(src=[1.1, 2.2, 3.3, 4.4], dst=z) return z @@ -263,7 +268,7 @@ class Structured2DQuadrilateralsGridModel(GridModel): # 2 # def get_grid_type(self, grid): - return 'structured_quadrilateral' + return "structured_quadrilateral" def get_grid_rank(self, grid: int) -> int: return 2 @@ -272,19 +277,20 @@ def get_grid_size(self, grid): return 4 def get_grid_shape(self, grid, shape): - numpy.copyto(src=[2, 2], dst=shape) + np.copyto(src=[2, 2], dst=shape) return shape def get_grid_x(self, grid, x): - numpy.copyto(src=[1.1, 0.1, 1.1, 2.1], dst=x) + np.copyto(src=[1.1, 0.1, 1.1, 2.1], dst=x) return x def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: - numpy.copyto(src=[2.2, 1.2, 0.2, 2.2], dst=y) + np.copyto(src=[2.2, 1.2, 0.2, 2.2], dst=y) return y def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: - raise NotImplementedError('Do not know what z is') + msg = "Do not know what z is" + raise NotImplementedError(msg) class UnstructuredGridBmiModel(GridModel): @@ -299,10 +305,11 @@ class UnstructuredGridBmiModel(GridModel): # 1---\ / # 4 def get_grid_type(self, grid): - return 'unstructured' + return "unstructured" def get_grid_shape(self, grid, dest): - raise NotImplementedError('Do not know what shape is') + msg = "Do not know what shape is" + raise NotImplementedError(msg) def get_grid_size(self, grid): return 6 @@ -320,38 +327,41 @@ def get_grid_face_count(self, grid: int) -> int: return 3 def get_grid_edge_nodes(self, grid: int, edge_nodes: np.ndarray) -> np.ndarray: - numpy.copyto(src=(0, 1, 1, 2, 2, 3, 3, 0, 1, 4, 4, 5, 5, 2, 5, 3), dst=edge_nodes) + np.copyto(src=(0, 1, 1, 2, 2, 3, 3, 0, 1, 4, 4, 5, 5, 2, 5, 3), dst=edge_nodes) return edge_nodes def get_grid_face_nodes(self, grid: int, face_nodes: np.ndarray) -> np.ndarray: - numpy.copyto(src=(0, 1, 2, 3, 1, 4, 5, 2, 2, 5, 3), dst=face_nodes) + np.copyto(src=(0, 1, 2, 3, 1, 4, 5, 2, 2, 5, 3), dst=face_nodes) return face_nodes def get_grid_face_edges(self, grid: int, face_edges: np.ndarray) -> np.ndarray: - numpy.copyto(src=(0, 1, 2, 3, 4, 5, 6, 1, 6, 7, 2), dst=face_edges) + np.copyto(src=(0, 1, 2, 3, 4, 5, 6, 1, 6, 7, 2), dst=face_edges) return face_edges - def get_grid_nodes_per_face(self, grid: int, nodes_per_face: np.ndarray) -> np.ndarray: - numpy.copyto(src=(4, 4, 3), dst=nodes_per_face) + def get_grid_nodes_per_face( + self, grid: int, nodes_per_face: np.ndarray + ) -> np.ndarray: + np.copyto(src=(4, 4, 3), dst=nodes_per_face) return nodes_per_face def get_grid_x(self, grid: int, x: np.ndarray) -> np.ndarray: - numpy.copyto(src=[0., 1., 2., 1., 3., 4.], dst=x) + np.copyto(src=[0.0, 1.0, 2.0, 1.0, 3.0, 4.0], dst=x) return x def get_grid_y(self, grid: int, y: np.ndarray) -> np.ndarray: - numpy.copyto(src=[3., 1., 2., 4., 0., 3.], dst=y) + np.copyto(src=[3.0, 1.0, 2.0, 4.0, 0.0, 3.0], dst=y) return y def get_grid_z(self, grid: int, z: np.ndarray) -> np.ndarray: - raise NotImplementedError('Do not know what z is') + msg = "Do not know what z is" + raise NotImplementedError(msg) class DTypeModel(GridModel): def __init__(self): super().__init__() - self.dtype = numpy.dtype('float32') - self.value = numpy.array((1.1, 2.2, 3.3), dtype=self.dtype) + self.dtype = np.dtype("float32") + self.value = np.array((1.1, 2.2, 3.3), dtype=self.dtype) def get_var_type(self, name): return str(self.dtype) @@ -363,11 +373,11 @@ def get_var_nbytes(self, name): return self.dtype.itemsize * self.value.size def get_value(self, name, dest): - numpy.copyto(src=self.value, dst=dest) + np.copyto(src=self.value, dst=dest) return dest def get_value_at_indices(self, name, dest, inds): - numpy.copyto(src=self.value[inds], dst=dest) + np.copyto(src=self.value[inds], dst=dest) return dest def set_value(self, name, src): @@ -384,15 +394,15 @@ class Float32Model(DTypeModel): class Int32Model(DTypeModel): def __init__(self): super().__init__() - self.dtype = numpy.dtype('int32') - self.value = numpy.array((12, 24, 36), dtype=self.dtype) + self.dtype = np.dtype("int32") + self.value = np.array((12, 24, 36), dtype=self.dtype) class BooleanModel(DTypeModel): def __init__(self): super().__init__() - self.dtype = numpy.dtype('bool') - self.value = numpy.array((True, False, True), dtype=self.dtype) + self.dtype = np.dtype("bool") + self.value = np.array((True, False, True), dtype=self.dtype) class HugeModel(DTypeModel): @@ -404,17 +414,20 @@ class HugeModel(DTypeModel): run-bmi-server --path $PWD/test --name fake_models.HugeModel --port 55555 --debug """ + def __init__(self): super().__init__() - self.dtype = numpy.dtype('float64') + self.dtype = np.dtype("float64") # Create value which is bigger than 4Mb dimension = (3 * 4_000_000) // self.dtype.itemsize + 1000 - self.value = numpy.ones((dimension,), dtype=self.dtype) + self.value = np.ones((dimension,), dtype=self.dtype) + class WithItemSizeZeroAndVarTypeFloat32Model(Float32Model): def get_var_itemsize(self, name): return 0 + class WithItemSizeZeroAndUnknownVarType(WithItemSizeZeroAndVarTypeFloat32Model): def get_var_type(self, name): - return 'real' \ No newline at end of file + return "real" From 2f17bf3cc9c1149d31fb085418525a78c947d244 Mon Sep 17 00:00:00 2001 From: sverhoeven Date: Thu, 7 Nov 2024 16:07:37 +0100 Subject: [PATCH 30/30] Make ruff check happy --- python/remotebmi/client/apptainer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/remotebmi/client/apptainer.py b/python/remotebmi/client/apptainer.py index 983ea97..1ef9e08 100644 --- a/python/remotebmi/client/apptainer.py +++ b/python/remotebmi/client/apptainer.py @@ -57,9 +57,9 @@ def __init__( stdout = subprocess.DEVNULL self.container = subprocess.Popen( # noqa: S603 args, - preexec_fn=os.setsid, + preexec_fn=os.setsid, # noqa: PLW1509 stderr=subprocess.STDOUT, - stdout=stdout, # noqa: PLW1509 if absent leaves zombie processes behind + stdout=stdout, ) time.sleep(delay) returncode = self.container.poll()