From 4d24d46113d65a94d8e6a76cd2131e2b52e159d9 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Fri, 9 Feb 2024 17:24:45 +0000 Subject: [PATCH 01/43] initial commit of container runtime environment package --- CMakeLists.txt | 4 + packages/cre/CMakeLists.txt | 40 ++++++++ packages/cre/ContainerRunner.cpp | 109 +++++++++++++++++++++ packages/cre/ContainerRunner.h | 83 ++++++++++++++++ packages/cre/CreParms.cpp | 145 ++++++++++++++++++++++++++++ packages/cre/CreParms.h | 85 ++++++++++++++++ packages/cre/README.md | 3 + packages/cre/cre.cpp | 93 ++++++++++++++++++ packages/cre/cre.h | 53 ++++++++++ project-config.cmake | 1 + targets/server-linux/SlideRule.cpp | 12 +++ targets/slideruleearth-aws/Makefile | 2 + 12 files changed, 630 insertions(+) create mode 100644 packages/cre/CMakeLists.txt create mode 100644 packages/cre/ContainerRunner.cpp create mode 100644 packages/cre/ContainerRunner.h create mode 100644 packages/cre/CreParms.cpp create mode 100644 packages/cre/CreParms.h create mode 100644 packages/cre/README.md create mode 100644 packages/cre/cre.cpp create mode 100644 packages/cre/cre.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c8d2fd692..f0c8c37c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,10 @@ if(${USE_CCSDS_PACKAGE}) add_subdirectory (packages/ccsds) endif() +if(${USE_CRE_PACKAGE}) + add_subdirectory (packages/cre) +endif() + if(${USE_GEO_PACKAGE}) add_subdirectory (packages/geo) endif() diff --git a/packages/cre/CMakeLists.txt b/packages/cre/CMakeLists.txt new file mode 100644 index 000000000..0f8b0608b --- /dev/null +++ b/packages/cre/CMakeLists.txt @@ -0,0 +1,40 @@ +# Find cURL Library +find_package (CURL) + +# Build package +if (CURL_FOUND) + + message (STATUS "Including cre package") + + target_compile_definitions (slideruleLib PUBLIC __cre__) + + target_link_libraries (slideruleLib PUBLIC ${CURL_LIBRARIES}) + target_include_directories (slideruleLib PUBLIC ${CURL_INCLUDE_DIR}) + + target_sources(slideruleLib + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/cre.cpp + ${CMAKE_CURRENT_LIST_DIR}/CreParms.cpp + ${CMAKE_CURRENT_LIST_DIR}/ContainerRunner.cpp + ) + + target_include_directories (slideruleLib + PUBLIC + $ + $ + ) + + install ( + FILES + ${CMAKE_CURRENT_LIST_DIR}/cre.h + ${CMAKE_CURRENT_LIST_DIR}/CreParms.h + ${CMAKE_CURRENT_LIST_DIR}/ContainerRunner.h + DESTINATION + ${INCDIR} + ) + +else () + + message (FATAL_ERROR "Unable to include cre package... required libraries not found") + +endif () diff --git a/packages/cre/ContainerRunner.cpp b/packages/cre/ContainerRunner.cpp new file mode 100644 index 000000000..021811322 --- /dev/null +++ b/packages/cre/ContainerRunner.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2021, University of Washington + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the University of Washington nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF WASHINGTON AND CONTRIBUTORS + * “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF WASHINGTON OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/****************************************************************************** + * INCLUDES + ******************************************************************************/ + +#include "ContainerRunner.h" +#include "OsApi.h" + +/****************************************************************************** + * STATIC DATA + ******************************************************************************/ + +const char* ContainerRunner::OBJECT_TYPE = "ContainerRunner"; +const char* ContainerRunner::LUA_META_NAME = "ContainerRunner"; +const struct luaL_Reg ContainerRunner::LUA_META_TABLE[] = { + {NULL, NULL} +}; + + +/****************************************************************************** + * PUBLIC METHODS + ******************************************************************************/ + +/*---------------------------------------------------------------------------- + * luaCreate - :container() + *----------------------------------------------------------------------------*/ +int ContainerRunner::luaCreate (lua_State* L) +{ + CreParms* _parms = NULL; + + try + { + /* Get Parameters */ + _parms = dynamic_cast(getLuaObject(L, 1, CreParms::OBJECT_TYPE)); + + /* Create Dispatch */ + return createLuaObject(L, new ContainerRunner(L, _parms)); + } + catch(const RunTimeException& e) + { + if(_parms) _parms->releaseLuaObject(); + mlog(e.level(), "Error creating %s: %s", LUA_META_NAME, e.what()); + return returnLuaStatus(L, false); + } +} + +/*---------------------------------------------------------------------------- + * init + *----------------------------------------------------------------------------*/ +void ContainerRunner::init (void) +{ +} + +/*---------------------------------------------------------------------------- + * deinit + *----------------------------------------------------------------------------*/ +void ContainerRunner::deinit (void) +{ +} + +/****************************************************************************** + * PRIVATE METHODS + ******************************************************************************/ + +/*---------------------------------------------------------------------------- + * Constructor + *----------------------------------------------------------------------------*/ +ContainerRunner::ContainerRunner (lua_State* L, const CreParms* _parms): + LuaObject(L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), + parms(_parms) +{ +} + +/*---------------------------------------------------------------------------- + * Destructor + *----------------------------------------------------------------------------*/ +ContainerRunner::~ContainerRunner (void) +{ +} diff --git a/packages/cre/ContainerRunner.h b/packages/cre/ContainerRunner.h new file mode 100644 index 000000000..109a47009 --- /dev/null +++ b/packages/cre/ContainerRunner.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021, University of Washington + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the University of Washington nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF WASHINGTON AND CONTRIBUTORS + * “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF WASHINGTON OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __container_runner__ +#define __container_runner__ + +/****************************************************************************** + * INCLUDES + ******************************************************************************/ + +#include "OsApi.h" +#include "LuaObject.h" +#include "CreParms.h" + +/****************************************************************************** + * CONTAINER RUNNER CLASS + ******************************************************************************/ + +class ContainerRunner: public LuaObject +{ + public: + + /*-------------------------------------------------------------------- + * Constants + *--------------------------------------------------------------------*/ + + static const char* OBJECT_TYPE; + static const char* LUA_META_NAME; + static const struct luaL_Reg LUA_META_TABLE[]; + + /*-------------------------------------------------------------------- + * Data + *--------------------------------------------------------------------*/ + + const CreParms* parms; + + /*-------------------------------------------------------------------- + * Methods + *--------------------------------------------------------------------*/ + + static int luaCreate (lua_State* L); + static void init (void); + static void deinit (void); + + private: + + /*-------------------------------------------------------------------- + * Methods + *--------------------------------------------------------------------*/ + + ContainerRunner (lua_State* L, const CreParms* _parms); + virtual ~ContainerRunner (void); +}; + +#endif /* __container_runner__ */ diff --git a/packages/cre/CreParms.cpp b/packages/cre/CreParms.cpp new file mode 100644 index 000000000..3d646cb88 --- /dev/null +++ b/packages/cre/CreParms.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2021, University of Washington + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the University of Washington nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF WASHINGTON AND CONTRIBUTORS + * “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF WASHINGTON OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/****************************************************************************** + * INCLUDES + ******************************************************************************/ + +#include "core.h" +#include "CreParms.h" + +/****************************************************************************** + * STATIC DATA + ******************************************************************************/ + +const char* CreParms::SELF = "output"; +const char* CreParms::IMAGE = "image"; + +const char* CreParms::OBJECT_TYPE = "CreParms"; +const char* CreParms::LUA_META_NAME = "CreParms"; +const struct luaL_Reg CreParms::LUA_META_TABLE[] = { + {"image", luaImage}, + {NULL, NULL} +}; + +/****************************************************************************** + * PUBLIC METHODS + ******************************************************************************/ + +/*---------------------------------------------------------------------------- + * luaCreate - create() + *----------------------------------------------------------------------------*/ +int CreParms::luaCreate (lua_State* L) +{ + try + { + /* Check if Lua Table */ + if(lua_type(L, 1) != LUA_TTABLE) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Cre parameters must be supplied as a lua table"); + } + + /* Return Request Parameter Object */ + return createLuaObject(L, new CreParms(L, 1)); + } + catch(const RunTimeException& e) + { + mlog(e.level(), "Error creating %s: %s", LUA_META_NAME, e.what()); + return returnLuaStatus(L, false); + } +} + +/*---------------------------------------------------------------------------- + * Constructor + *----------------------------------------------------------------------------*/ +CreParms::CreParms (lua_State* L, int index): + LuaObject (L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), + image (NULL) +{ + /* Populate Object from Lua */ + try + { + /* Must be a Table */ + if(lua_istable(L, index)) + { + bool field_provided = false; + + /* Output Path */ + lua_getfield(L, index, IMAGE); + image = StringLib::duplicate(LuaObject::getLuaString(L, -1, true, image, &field_provided)); + if(field_provided) mlog(DEBUG, "Setting %s to %s", IMAGE, image); + lua_pop(L, 1); + } + } + catch(const RunTimeException& e) + { + cleanup(); + throw; + } +} + +/*---------------------------------------------------------------------------- + * Destructor + *----------------------------------------------------------------------------*/ +CreParms::~CreParms (void) +{ + cleanup(); +} + +/*---------------------------------------------------------------------------- + * cleanup + *----------------------------------------------------------------------------*/ +void CreParms::cleanup (void) +{ + if(image) + { + delete [] image; + image = NULL; + } +} + +/*---------------------------------------------------------------------------- + * luaImage + *----------------------------------------------------------------------------*/ +int CreParms::luaImage (lua_State* L) +{ + try + { + CreParms* lua_obj = dynamic_cast(getLuaSelf(L, 1)); + if(lua_obj->image) lua_pushstring(L, lua_obj->image); + else lua_pushnil(L); + return 1; + } + catch(const RunTimeException& e) + { + return luaL_error(L, "method invoked from invalid object: %s", __FUNCTION__); + } +} diff --git a/packages/cre/CreParms.h b/packages/cre/CreParms.h new file mode 100644 index 000000000..0dc7c908a --- /dev/null +++ b/packages/cre/CreParms.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2021, University of Washington + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the University of Washington nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF WASHINGTON AND CONTRIBUTORS + * “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF WASHINGTON OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __cre_parms__ +#define __cre_parms__ + +/****************************************************************************** + * INCLUDES + ******************************************************************************/ + +#include "OsApi.h" +#include "LuaObject.h" + +/****************************************************************************** + * CRE PARAMETERS CLASS + ******************************************************************************/ + +class CreParms: public LuaObject +{ + public: + + /*-------------------------------------------------------------------- + * Constants + *--------------------------------------------------------------------*/ + + static const char* SELF; + static const char* IMAGE; + + static const char* OBJECT_TYPE; + static const char* LUA_META_NAME; + static const struct luaL_Reg LUA_META_TABLE[]; + + /*-------------------------------------------------------------------- + * Data + *--------------------------------------------------------------------*/ + + const char* image; // container image + + /*-------------------------------------------------------------------- + * Methods + *--------------------------------------------------------------------*/ + + static int luaCreate (lua_State* L); + CreParms (lua_State* L, int index); + ~CreParms (void); + + private: + + /*-------------------------------------------------------------------- + * Methods + *--------------------------------------------------------------------*/ + + void cleanup (void); + static int luaImage (lua_State* L); +}; + +#endif /* __cre_parms__ */ diff --git a/packages/cre/README.md b/packages/cre/README.md new file mode 100644 index 000000000..171e41117 --- /dev/null +++ b/packages/cre/README.md @@ -0,0 +1,3 @@ +# Container Runtime Environment (CRE) Package + +This package provides classes for running containers. diff --git a/packages/cre/cre.cpp b/packages/cre/cre.cpp new file mode 100644 index 000000000..90ed40a71 --- /dev/null +++ b/packages/cre/cre.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2021, University of Washington + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the University of Washington nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF WASHINGTON AND CONTRIBUTORS + * “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF WASHINGTON OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/****************************************************************************** + *INCLUDES + ******************************************************************************/ + +#include "core.h" +#include "cre.h" + +/****************************************************************************** + * DEFINES + ******************************************************************************/ + +#define LUA_CRE_LIBNAME "cre" + +/****************************************************************************** + * LOCAL FUNCTIONS + ******************************************************************************/ + +/*---------------------------------------------------------------------------- + * cre_open + *----------------------------------------------------------------------------*/ +int cre_open (lua_State* L) +{ + static const struct luaL_Reg cre_functions[] = { + {"container", ContainerRunner::luaCreate}, + {"parms", CreParms::luaCreate}, + {NULL, NULL} + }; + + /* Set Library */ + luaL_newlib(L, cre_functions); + + /* Set Globals */ + LuaEngine::setAttrStr(L, "PARMS", CreParms::SELF); + + return 1; +} + +/****************************************************************************** + * EXPORTED FUNCTIONS + ******************************************************************************/ + +extern "C" { +void initcre (void) +{ + /* Initialize Modules */ + ContainerRunner::init(); + + /* Extend Lua */ + LuaEngine::extend(LUA_CRE_LIBNAME, cre_open); + + /* Indicate Presence of Package */ + LuaEngine::indicate(LUA_CRE_LIBNAME, LIBID); + + /* Display Status */ + print2term("%s package initialized (%s)\n", LUA_CRE_LIBNAME, LIBID); +} + +void deinitcre (void) +{ + ContainerRunner::deinit(); +} +} diff --git a/packages/cre/cre.h b/packages/cre/cre.h new file mode 100644 index 000000000..ba2c97754 --- /dev/null +++ b/packages/cre/cre.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021, University of Washington + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the University of Washington nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF WASHINGTON AND CONTRIBUTORS + * “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF WASHINGTON OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __crepkg__ +#define __crepkg__ + +/****************************************************************************** + * INCLUDES + ******************************************************************************/ + +#include "ContainerRunner.h" +#include "CreParms.h" + +/****************************************************************************** + * PROTOTYPES + ******************************************************************************/ + +extern "C" { +void initcre (void); +void deinitcre (void); +} + +#endif /* __crepkg__ */ + + diff --git a/project-config.cmake b/project-config.cmake index 6bf85b51a..600fe8869 100644 --- a/project-config.cmake +++ b/project-config.cmake @@ -95,6 +95,7 @@ option (ENABLE_BEST_EFFORT_CONDA_ENV "Attempt to alleviate some issues with runn option (USE_ARROW_PACKAGE "Include the Apache Arrow package" OFF) option (USE_AWS_PACKAGE "Include the AWS package" OFF) option (USE_CCSDS_PACKAGE "Include the CCSDS package" ON) +option (USE_CRE_PACKAGE "Include the CRE package" OFF) option (USE_GEO_PACKAGE "Include the GEO package" OFF) option (USE_H5_PACKAGE "Include the HDF5 package" ON) option (USE_LEGACY_PACKAGE "Include the Legacy package" ON) diff --git a/targets/server-linux/SlideRule.cpp b/targets/server-linux/SlideRule.cpp index 3707ceb54..4eb3a0399 100644 --- a/targets/server-linux/SlideRule.cpp +++ b/targets/server-linux/SlideRule.cpp @@ -47,6 +47,10 @@ #include "ccsds.h" #endif +#ifdef __cre__ +#include "cre.h" +#endif + #ifdef __geo__ #include "geo.h" #endif @@ -302,6 +306,10 @@ int main (int argc, char* argv[]) initccsds(); #endif + #ifdef __cre__ + initcre(); + #endif + #ifdef __geo__ initgeo(); #endif @@ -379,6 +387,10 @@ int main (int argc, char* argv[]) deinitccsds(); #endif + #ifdef __cre__ + deinitcre(); + #endif + #ifdef __aws__ deinitaws(); #endif diff --git a/targets/slideruleearth-aws/Makefile b/targets/slideruleearth-aws/Makefile index f96df2e56..3e4fc20b3 100644 --- a/targets/slideruleearth-aws/Makefile +++ b/targets/slideruleearth-aws/Makefile @@ -88,6 +88,7 @@ DEBUG_CFG += -DENABLE_ADDRESS_SANITIZER=ON DEBUG_CFG += -DUSE_ARROW_PACKAGE=ON DEBUG_CFG += -DUSE_AWS_PACKAGE=ON DEBUG_CFG += -DUSE_CCSDS_PACKAGE=ON +DEBUG_CFG += -DUSE_CRE_PACKAGE=ON DEBUG_CFG += -DUSE_GEO_PACKAGE=ON DEBUG_CFG += -DUSE_H5_PACKAGE=ON DEBUG_CFG += -DUSE_LEGACY_PACKAGE=ON @@ -99,6 +100,7 @@ RELEASE_CFG := -DCMAKE_BUILD_TYPE=Release RELEASE_CFG += -DINSTALLDIR=$(INSTALL_DIR) RELEASE_CFG += -DUSE_ARROW_PACKAGE=ON RELEASE_CFG += -DUSE_AWS_PACKAGE=ON +RELEASE_CFG += -DUSE_CRE_PACKAGE=ON RELEASE_CFG += -DUSE_GEO_PACKAGE=ON RELEASE_CFG += -DUSE_H5_PACKAGE=ON RELEASE_CFG += -DUSE_NETSVC_PACKAGE=ON From 1c65879441a9913203a466a02e91ce3b9e5a55c8 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Fri, 9 Feb 2024 21:09:48 +0000 Subject: [PATCH 02/43] basic return from cre lua object --- packages/cre/ContainerRunner.cpp | 76 +++++++++++++++++++++++++++++++- packages/cre/ContainerRunner.h | 26 ++++++++--- packages/cre/CreParms.cpp | 12 ++++- packages/cre/CreParms.h | 4 ++ scripts/CMakeLists.txt | 1 + scripts/endpoints/cre.lua | 21 +++++++++ 6 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 scripts/endpoints/cre.lua diff --git a/packages/cre/ContainerRunner.cpp b/packages/cre/ContainerRunner.cpp index 021811322..19c1f1863 100644 --- a/packages/cre/ContainerRunner.cpp +++ b/packages/cre/ContainerRunner.cpp @@ -43,6 +43,7 @@ const char* ContainerRunner::OBJECT_TYPE = "ContainerRunner"; const char* ContainerRunner::LUA_META_NAME = "ContainerRunner"; const struct luaL_Reg ContainerRunner::LUA_META_TABLE[] = { + {"result", ContainerRunner::luaResult}, {NULL, NULL} }; @@ -97,8 +98,11 @@ void ContainerRunner::deinit (void) *----------------------------------------------------------------------------*/ ContainerRunner::ContainerRunner (lua_State* L, const CreParms* _parms): LuaObject(L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), - parms(_parms) + parms(_parms), + result(NULL) { + active = true; + controlPid = new Thread(controlThread, this); } /*---------------------------------------------------------------------------- @@ -106,4 +110,74 @@ ContainerRunner::ContainerRunner (lua_State* L, const CreParms* _parms): *----------------------------------------------------------------------------*/ ContainerRunner::~ContainerRunner (void) { + active = false; + delete controlPid; + delete result; +} + +/*---------------------------------------------------------------------------- + * controlThread + *----------------------------------------------------------------------------*/ +void* ContainerRunner::controlThread (void* parm) +{ + ContainerRunner* cr = static_cast(parm); + cr->resultLock.lock(); + { + cr->result = StringLib::duplicate("Hello World"); + cr->resultLock.signal(RESULT_SIGNAL); + } + cr->resultLock.unlock(); + cr->signalComplete(); + return NULL; +} + + +/*---------------------------------------------------------------------------- + * luaResult - result() -> result string + *----------------------------------------------------------------------------*/ +int ContainerRunner::luaResult (lua_State* L) +{ + ContainerRunner* lua_obj = NULL; + bool status = false; + int num_ret = 1; + + try + { + /* Get Parameters */ + lua_obj = dynamic_cast(getLuaSelf(L, 1)); + + /* Get Result */ + lua_obj->resultLock.lock(); + { + /* Wait for Result */ + if(lua_obj->parms->timeout == IO_PEND) + { + while((lua_obj->result == NULL) && (lua_obj->active)) + { + lua_obj->resultLock.wait(RESULT_SIGNAL, SYS_TIMEOUT); + } + } + else if(lua_obj->parms->timeout > 0) + { + long timeout_ms = lua_obj->parms->timeout * 1000; + while((lua_obj->result == NULL) && (lua_obj->active) && (timeout_ms > 0)) + { + lua_obj->resultLock.wait(RESULT_SIGNAL, SYS_TIMEOUT); + timeout_ms -= SYS_TIMEOUT; + } + } + + /* Push Result */ + lua_pushstring(L, lua_obj->result); + status = lua_obj->result != NULL; + num_ret++; + } + lua_obj->resultLock.unlock(); + } + catch(const RunTimeException& e) + { + return luaL_error(L, "method invoked from invalid object: %s", __FUNCTION__); + } + + return returnLuaStatus(L, status, num_ret); } diff --git a/packages/cre/ContainerRunner.h b/packages/cre/ContainerRunner.h index 109a47009..854ce9f8d 100644 --- a/packages/cre/ContainerRunner.h +++ b/packages/cre/ContainerRunner.h @@ -56,6 +56,9 @@ class ContainerRunner: public LuaObject static const char* LUA_META_NAME; static const struct luaL_Reg LUA_META_TABLE[]; + static const int RESULT_SIGNAL = 0; + static const int DEFAULT_TIMEOUT = 600; + /*-------------------------------------------------------------------- * Data *--------------------------------------------------------------------*/ @@ -66,18 +69,31 @@ class ContainerRunner: public LuaObject * Methods *--------------------------------------------------------------------*/ - static int luaCreate (lua_State* L); - static void init (void); - static void deinit (void); + static int luaCreate (lua_State* L); + static void init (void); + static void deinit (void); private: + /*-------------------------------------------------------------------- + * Data + *--------------------------------------------------------------------*/ + + bool active; + Thread* controlPid; + const char* result; + Cond resultLock; + /*-------------------------------------------------------------------- * Methods *--------------------------------------------------------------------*/ - ContainerRunner (lua_State* L, const CreParms* _parms); - virtual ~ContainerRunner (void); + ContainerRunner (lua_State* L, const CreParms* _parms); + virtual ~ContainerRunner (void); + + static void* controlThread (void* parm); + + static int luaResult (lua_State* L); }; #endif /* __container_runner__ */ diff --git a/packages/cre/CreParms.cpp b/packages/cre/CreParms.cpp index 3d646cb88..a1aa6f6d1 100644 --- a/packages/cre/CreParms.cpp +++ b/packages/cre/CreParms.cpp @@ -42,6 +42,7 @@ const char* CreParms::SELF = "output"; const char* CreParms::IMAGE = "image"; +const char* CreParms::TIMEOUT = "timeout"; const char* CreParms::OBJECT_TYPE = "CreParms"; const char* CreParms::LUA_META_NAME = "CreParms"; @@ -82,7 +83,8 @@ int CreParms::luaCreate (lua_State* L) *----------------------------------------------------------------------------*/ CreParms::CreParms (lua_State* L, int index): LuaObject (L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), - image (NULL) + image (NULL), + timeout (DEFAULT_TIMEOUT) { /* Populate Object from Lua */ try @@ -92,11 +94,17 @@ CreParms::CreParms (lua_State* L, int index): { bool field_provided = false; - /* Output Path */ + /* Image */ lua_getfield(L, index, IMAGE); image = StringLib::duplicate(LuaObject::getLuaString(L, -1, true, image, &field_provided)); if(field_provided) mlog(DEBUG, "Setting %s to %s", IMAGE, image); lua_pop(L, 1); + + /* Timeout */ + lua_getfield(L, index, TIMEOUT); + timeout = LuaObject::getLuaInteger(L, -1, true, timeout, &field_provided); + if(field_provided) mlog(DEBUG, "Setting %s to %d", TIMEOUT, timeout); + lua_pop(L, 1); } } catch(const RunTimeException& e) diff --git a/packages/cre/CreParms.h b/packages/cre/CreParms.h index 0dc7c908a..8321bfa21 100644 --- a/packages/cre/CreParms.h +++ b/packages/cre/CreParms.h @@ -53,16 +53,20 @@ class CreParms: public LuaObject static const char* SELF; static const char* IMAGE; + static const char* TIMEOUT; static const char* OBJECT_TYPE; static const char* LUA_META_NAME; static const struct luaL_Reg LUA_META_TABLE[]; + static const int DEFAULT_TIMEOUT = 600; + /*-------------------------------------------------------------------- * Data *--------------------------------------------------------------------*/ const char* image; // container image + int timeout; /*-------------------------------------------------------------------- * Methods diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt index ebd9b18ec..e62a6fb2e 100644 --- a/scripts/CMakeLists.txt +++ b/scripts/CMakeLists.txt @@ -2,6 +2,7 @@ # Endpoints # install ( FILES + ${CMAKE_CURRENT_LIST_DIR}/endpoints/cre.lua ${CMAKE_CURRENT_LIST_DIR}/endpoints/definition.lua ${CMAKE_CURRENT_LIST_DIR}/endpoints/event.lua ${CMAKE_CURRENT_LIST_DIR}/selftests/example_engine_endpoint.lua diff --git a/scripts/endpoints/cre.lua b/scripts/endpoints/cre.lua new file mode 100644 index 000000000..9fe762fef --- /dev/null +++ b/scripts/endpoints/cre.lua @@ -0,0 +1,21 @@ +-- +-- ENDPOINT: /source/cre +-- +-- INPUT: arg[1] +-- { +-- "image": "" +-- "timeout": +-- } +-- +-- OUTPUT: +-- + +local json = require("json") +local parm = json.decode(arg[1]) + +local cre_parms = cre.parms(parm) +local cre_runner = cre.container(cre_parms) + +local result = cre_runner:result() + +return result From 6dae25ea23daad1507d3b0b79cf3183910e5ba1c Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Fri, 9 Feb 2024 22:37:59 +0000 Subject: [PATCH 03/43] added container list endpoint --- packages/cre/ContainerRunner.cpp | 61 +++++++++++++++++++++++++++++++- packages/cre/ContainerRunner.h | 1 + packages/cre/cre.cpp | 1 + packages/netsvc/CurlLib.cpp | 7 +++- packages/netsvc/CurlLib.h | 5 +-- scripts/CMakeLists.txt | 1 + scripts/endpoints/lscre.lua | 11 ++++++ 7 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 scripts/endpoints/lscre.lua diff --git a/packages/cre/ContainerRunner.cpp b/packages/cre/ContainerRunner.cpp index 19c1f1863..433782cc3 100644 --- a/packages/cre/ContainerRunner.cpp +++ b/packages/cre/ContainerRunner.cpp @@ -35,6 +35,8 @@ #include "ContainerRunner.h" #include "OsApi.h" +#include "CurlLib.h" // netsvc package dependency +#include "EndpointObject.h" /****************************************************************************** * STATIC DATA @@ -121,13 +123,45 @@ ContainerRunner::~ContainerRunner (void) void* ContainerRunner::controlThread (void* parm) { ContainerRunner* cr = static_cast(parm); + const char* result = NULL; + + /* Run Container */ + const char* unix_socket = "/var/run/docker.sock"; + const char* url= "http://localhost/v1.44/containers/create"; + FString data("{\"Image\": \"%s\"}", cr->parms->image); + List headers(5); + string* content_type = new string("Content-Type: application/json"); + headers.add(content_type); + const char* response = NULL; + long http_code = CurlLib::request(EndpointObject::POST, url, data.c_str(), &response, NULL, false, false, &headers, unix_socket); + if(http_code != EndpointObject::OK) + { + mlog(CRITICAL, "Failed to start container <%s>: %s", cr->parms->image, response); + } + + /* Clean Up Response */ + delete [] response; + response = NULL; + + /* Wait for Completion and Get Result */ + if(http_code == EndpointObject::OK) + { + /* Poll Completion of Container */ + // using timeout + + /* Get Result */ + // from well known file + } + + /* Signal Complete */ cr->resultLock.lock(); { - cr->result = StringLib::duplicate("Hello World"); + cr->result = result; cr->resultLock.signal(RESULT_SIGNAL); } cr->resultLock.unlock(); cr->signalComplete(); + return NULL; } @@ -181,3 +215,28 @@ int ContainerRunner::luaResult (lua_State* L) return returnLuaStatus(L, status, num_ret); } + +/*---------------------------------------------------------------------------- + * luaList - list() -> json of running containers + *----------------------------------------------------------------------------*/ +int ContainerRunner::luaList (lua_State* L) +{ + const char* unix_socket = "/var/run/docker.sock"; + const char* url= "http://localhost/v1.43/containers/json"; + + /* Make Request for List of Containers */ + const char* response = NULL; + int size = 0; + long http_code = CurlLib::request(EndpointObject::GET, url, NULL, &response, &size, false, false, NULL, unix_socket); + + /* Push Result */ + lua_pushinteger(L, http_code); + lua_pushinteger(L, size); + lua_pushstring(L, response); + + /* Clean Up */ + delete [] response; + + /* Return */ + return returnLuaStatus(L, true, 4); +} diff --git a/packages/cre/ContainerRunner.h b/packages/cre/ContainerRunner.h index 854ce9f8d..60585befa 100644 --- a/packages/cre/ContainerRunner.h +++ b/packages/cre/ContainerRunner.h @@ -72,6 +72,7 @@ class ContainerRunner: public LuaObject static int luaCreate (lua_State* L); static void init (void); static void deinit (void); + static int luaList (lua_State* L); private: diff --git a/packages/cre/cre.cpp b/packages/cre/cre.cpp index 90ed40a71..f2119c3bb 100644 --- a/packages/cre/cre.cpp +++ b/packages/cre/cre.cpp @@ -53,6 +53,7 @@ int cre_open (lua_State* L) { static const struct luaL_Reg cre_functions[] = { {"container", ContainerRunner::luaCreate}, + {"list", ContainerRunner::luaList}, {"parms", CreParms::luaCreate}, {NULL, NULL} }; diff --git a/packages/netsvc/CurlLib.cpp b/packages/netsvc/CurlLib.cpp index 837f4cef4..f95b27ecf 100644 --- a/packages/netsvc/CurlLib.cpp +++ b/packages/netsvc/CurlLib.cpp @@ -62,7 +62,7 @@ void CurlLib::deinit (void) /*---------------------------------------------------------------------------- * request *----------------------------------------------------------------------------*/ -long CurlLib::request (EndpointObject::verb_t verb, const char* url, const char* data, const char** response, int* size, bool verify_peer, bool verify_hostname, List* headers) +long CurlLib::request (EndpointObject::verb_t verb, const char* url, const char* data, const char** response, int* size, bool verify_peer, bool verify_hostname, List* headers, const char* unix_socket) { long http_code = 0; CURL* curl = NULL; @@ -100,6 +100,11 @@ long CurlLib::request (EndpointObject::verb_t verb, const char* url, const char* curl_easy_setopt(curl, CURLOPT_COOKIEJAR, ".cookies"); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + if(unix_socket) + { + curl_easy_setopt(curl, CURLOPT_UNIX_SOCKET_PATH, (char*)unix_socket); + } + if(verb == EndpointObject::GET && rqst.size > 0) { curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); diff --git a/packages/netsvc/CurlLib.h b/packages/netsvc/CurlLib.h index 9568439d5..383423026 100644 --- a/packages/netsvc/CurlLib.h +++ b/packages/netsvc/CurlLib.h @@ -56,10 +56,7 @@ class CurlLib static void init (void); static void deinit (void); - static long request (EndpointObject::verb_t verb, const char* url, const char* data, const char** response, int* size, bool verify_peer=false, bool verify_hostname=false, List* headers=NULL); - static long get (const char* url, const char* data, const char** response, int* size, bool verify_peer=false, bool verify_hostname=false); - static long put (const char* url, const char* data, const char** response, int* size, bool verify_peer=false, bool verify_hostname=false); - static long post (const char* url, const char* data, const char** response, int* size, bool verify_peer=false, bool verify_hostname=false); + static long request (EndpointObject::verb_t verb, const char* url, const char* data, const char** response, int* size, bool verify_peer=false, bool verify_hostname=false, List* headers=NULL, const char* unix_socket=NULL); static long postAsStream (const char* url, const char* data, Publisher* outq, bool with_terminator); static long postAsRecord (const char* url, const char* data, Publisher* outq, bool with_terminator, int timeout, bool* active=NULL); static int getHeaders (lua_State* L, int index, List& header_list); diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt index e62a6fb2e..ffbe8f20d 100644 --- a/scripts/CMakeLists.txt +++ b/scripts/CMakeLists.txt @@ -12,6 +12,7 @@ install ( ${CMAKE_CURRENT_LIST_DIR}/endpoints/h5p.lua ${CMAKE_CURRENT_LIST_DIR}/endpoints/health.lua ${CMAKE_CURRENT_LIST_DIR}/endpoints/index.lua + ${CMAKE_CURRENT_LIST_DIR}/endpoints/lscre.lua ${CMAKE_CURRENT_LIST_DIR}/endpoints/metric.lua ${CMAKE_CURRENT_LIST_DIR}/endpoints/prometheus.lua ${CMAKE_CURRENT_LIST_DIR}/endpoints/samples.lua diff --git a/scripts/endpoints/lscre.lua b/scripts/endpoints/lscre.lua new file mode 100644 index 000000000..ad6062783 --- /dev/null +++ b/scripts/endpoints/lscre.lua @@ -0,0 +1,11 @@ +-- +-- ENDPOINT: /source/cre +-- +-- INPUT: None +-- +-- OUTPUT: +-- + +local http_code, size, response, status = cre.list() +print(status, http_code, size, response) +return response From 432a763c82806219ee591a8df5f1fcd53ad74d7e Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Tue, 13 Feb 2024 19:02:34 +0000 Subject: [PATCH 04/43] hello world for python container --- packages/cre/ContainerRunner.cpp | 47 +++- packages/cre/ContainerRunner.h | 12 +- packages/cre/CreParms.cpp | 21 +- packages/cre/cre.cpp | 1 + scripts/apps/server.lua | 6 + targets/python-container/Dockerfile | 6 + .../python-container/conda-linux-aarch64.lock | 208 ++++++++++++++++++ targets/python-container/environment.yml | 11 + targets/python-container/helloworld.py | 3 + targets/slideruleearth-aws/Makefile | 18 ++ targets/slideruleearth-aws/docker-compose.yml | 1 + .../cluster/docker-compose-sliderule.yml | 1 + .../terraform/cluster/sliderule-asg.tf | 1 + 13 files changed, 322 insertions(+), 14 deletions(-) create mode 100644 targets/python-container/Dockerfile create mode 100644 targets/python-container/conda-linux-aarch64.lock create mode 100644 targets/python-container/environment.yml create mode 100755 targets/python-container/helloworld.py diff --git a/packages/cre/ContainerRunner.cpp b/packages/cre/ContainerRunner.cpp index 433782cc3..5447c1af7 100644 --- a/packages/cre/ContainerRunner.cpp +++ b/packages/cre/ContainerRunner.cpp @@ -49,6 +49,7 @@ const struct luaL_Reg ContainerRunner::LUA_META_TABLE[] = { {NULL, NULL} }; +const char* ContainerRunner::REGISTRY = NULL; /****************************************************************************** * PUBLIC METHODS @@ -91,6 +92,14 @@ void ContainerRunner::deinit (void) { } +/*---------------------------------------------------------------------------- + * getRegistry + *----------------------------------------------------------------------------*/ +const char* ContainerRunner::getRegistry (void) +{ + return REGISTRY; +} + /****************************************************************************** * PRIVATE METHODS ******************************************************************************/ @@ -128,20 +137,14 @@ void* ContainerRunner::controlThread (void* parm) /* Run Container */ const char* unix_socket = "/var/run/docker.sock"; const char* url= "http://localhost/v1.44/containers/create"; - FString data("{\"Image\": \"%s\"}", cr->parms->image); + FString data("{\"Image\": \"%s/%s\"}", REGISTRY, cr->parms->image); List headers(5); string* content_type = new string("Content-Type: application/json"); headers.add(content_type); const char* response = NULL; long http_code = CurlLib::request(EndpointObject::POST, url, data.c_str(), &response, NULL, false, false, &headers, unix_socket); - if(http_code != EndpointObject::OK) - { - mlog(CRITICAL, "Failed to start container <%s>: %s", cr->parms->image, response); - } - - /* Clean Up Response */ + if(http_code != EndpointObject::OK) mlog(CRITICAL, "Failed to start container <%s>: %s", cr->parms->image, response); delete [] response; - response = NULL; /* Wait for Completion and Get Result */ if(http_code == EndpointObject::OK) @@ -240,3 +243,31 @@ int ContainerRunner::luaList (lua_State* L) /* Return */ return returnLuaStatus(L, true, 4); } + +/*---------------------------------------------------------------------------- + * luaSetRegistry - setregistry() + * + * - MUST BE SET BEFORE FIRST USE + *----------------------------------------------------------------------------*/ +int ContainerRunner::luaSetRegistry (lua_State* L) +{ + bool status = false; + try + { + /* Get Parameters */ + const char* registry_name = getLuaString(L, 1); + + /* Set Registry */ + if(REGISTRY == NULL) + { + REGISTRY = StringLib::duplicate(registry_name); + status = true; + } + } + catch(const RunTimeException& e) + { + mlog(e.level(), "Failed to set registry: %s", e.what()); + } + + return returnLuaStatus(L, status); +} diff --git a/packages/cre/ContainerRunner.h b/packages/cre/ContainerRunner.h index 60585befa..8b97c48e3 100644 --- a/packages/cre/ContainerRunner.h +++ b/packages/cre/ContainerRunner.h @@ -69,10 +69,12 @@ class ContainerRunner: public LuaObject * Methods *--------------------------------------------------------------------*/ - static int luaCreate (lua_State* L); - static void init (void); - static void deinit (void); - static int luaList (lua_State* L); + static int luaCreate (lua_State* L); + static void init (void); + static void deinit (void); + static const char* getRegistry (void); + static int luaList (lua_State* L); + static int luaSetRegistry (lua_State* L); private: @@ -80,6 +82,8 @@ class ContainerRunner: public LuaObject * Data *--------------------------------------------------------------------*/ + static const char* REGISTRY; + bool active; Thread* controlPid; const char* result; diff --git a/packages/cre/CreParms.cpp b/packages/cre/CreParms.cpp index a1aa6f6d1..5388cfe79 100644 --- a/packages/cre/CreParms.cpp +++ b/packages/cre/CreParms.cpp @@ -96,8 +96,25 @@ CreParms::CreParms (lua_State* L, int index): /* Image */ lua_getfield(L, index, IMAGE); - image = StringLib::duplicate(LuaObject::getLuaString(L, -1, true, image, &field_provided)); - if(field_provided) mlog(DEBUG, "Setting %s to %s", IMAGE, image); + const char* _image = StringLib::duplicate(LuaObject::getLuaString(L, -1, true, image, &field_provided)); + if(field_provided) + { + /* Check Image for ONLY Legal Characters */ + string s(_image); + for (auto c_iter = s.begin(); c_iter < s.end(); ++c_iter) + { + int c = *c_iter; + if(!isalnum(c) && (c != '/') && (c != '.') && (c != '-')) + { + delete [] _image; + throw RunTimeException(CRITICAL, RTE_ERROR, "invalid character found in image name: %c", c); + } + } + + /* Set Image */ + image = _image; + mlog(DEBUG, "Setting %s to %s", IMAGE, image); + } lua_pop(L, 1); /* Timeout */ diff --git a/packages/cre/cre.cpp b/packages/cre/cre.cpp index f2119c3bb..1288122fd 100644 --- a/packages/cre/cre.cpp +++ b/packages/cre/cre.cpp @@ -54,6 +54,7 @@ int cre_open (lua_State* L) static const struct luaL_Reg cre_functions[] = { {"container", ContainerRunner::luaCreate}, {"list", ContainerRunner::luaList}, + {"setregistry", ContainerRunner::luaSetRegistry}, {"parms", CreParms::luaCreate}, {NULL, NULL} }; diff --git a/scripts/apps/server.lua b/scripts/apps/server.lua index 56cd9eeef..868527b43 100644 --- a/scripts/apps/server.lua +++ b/scripts/apps/server.lua @@ -38,6 +38,7 @@ local orchestrator_url = cfgtbl["orchestrator"] or os.getenv("ORCHESTRA local org_name = cfgtbl["cluster"] or os.getenv("CLUSTER") local ps_url = cfgtbl["provisioning_system"] or os.getenv("PROVISIONING_SYSTEM") local ps_auth = cfgtbl["authenticate_to_ps"] -- nil is false +local container_registry = cfgtbl["container_registry"] or os.getenv("CONTAINER_REGISTRY") -------------------------------------------------- -- System Configuration @@ -83,6 +84,11 @@ if authenticate_to_podaac then local earthdata_auth_script = core.script("earth_data_auth", json.encode(script_parms)):name("PodaacAuthScript") end +-- Configure Container Registry -- +if __cre__ then + cre.setregistry(container_registry) +end + -------------------------------------------------- -- Application Server -------------------------------------------------- diff --git a/targets/python-container/Dockerfile b/targets/python-container/Dockerfile new file mode 100644 index 000000000..b1b91765d --- /dev/null +++ b/targets/python-container/Dockerfile @@ -0,0 +1,6 @@ +FROM condaforge/mambaforge:4.9.2-5 as conda + +COPY conda-linux-aarch64.lock . +RUN mamba create --copy -p /env --file conda-linux-aarch64.lock && \ + conda clean -afy +RUN echo "source activate /env" > ~/.bashrc diff --git a/targets/python-container/conda-linux-aarch64.lock b/targets/python-container/conda-linux-aarch64.lock new file mode 100644 index 000000000..dfa20d2ab --- /dev/null +++ b/targets/python-container/conda-linux-aarch64.lock @@ -0,0 +1,208 @@ +# Generated by conda-lock. +# platform: linux-aarch64 +# input_hash: b1f118743ac6af4d4e472d90afbc78d1bc0cac209f8bfa143bd66ff3ac702532 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2024.2.2-hcefe29a_0.conda#57c226edb90c4e973b9b7503537dd339 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 +https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.40-h2d8c526_0.conda#16246d69e945d0b1969a6099e7c5d457 +https://conda.anaconda.org/conda-forge/linux-aarch64/libboost-headers-1.84.0-h8af1aa0_0.conda#6ee13ea4adf1595a70917ef0dc62d876 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-13.2.0-hf8544c7_5.conda#379be2f115ffb73860e4e260dd2170b7 +https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-13.2.0-h9a76618_5.conda#1b79d37dce0fad96bdf3de03925f43b4 +https://conda.anaconda.org/conda-forge/noarch/poppler-data-0.4.12-hd8ed1ab_0.conda#d8d7293c5b37f39b2ac32940621c6592 +https://conda.anaconda.org/conda-forge/linux-aarch64/python_abi-3.12-4_cp312.conda#6c09f8e580146d88f649780cebed01de +https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda#161081fc7cec0bfda0d86d7cb595f8d8 +https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2#6168d71addc746e8f2b8d57dfd2edcea +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab +https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-13.2.0-hf8544c7_5.conda#dee934e640275d9e74e7bbd455f25162 +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-common-0.9.12-h31becfc_0.conda#6f917de3433c28ef387d1b2df5f6a624 +https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h31becfc_5.conda#a64e35f01e0b7a2a152eca87d33b9c87 +https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.26.0-h31becfc_0.conda#f5094fec0d7d788152c7503140929bf2 +https://conda.anaconda.org/conda-forge/linux-aarch64/geos-3.12.1-h2f0025b_0.conda#ac30e662102643639f9421aa80723e2b +https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-0.21.1-ha18d298_0.tar.bz2#b109f1a4d22966793d61fd7f75b744c3 +https://conda.anaconda.org/conda-forge/linux-aarch64/gflags-2.2.2-h54f1f3f_1004.tar.bz2#f286d3464cc8d467c92e4f17990c98c1 +https://conda.anaconda.org/conda-forge/linux-aarch64/giflib-5.2.1-hb4cce97_3.conda#a1f16c57cf6c50399556a32e750e9461 +https://conda.anaconda.org/conda-forge/linux-aarch64/icu-73.2-h787c7f5_0.conda#9d3c29d71f28452a2e843aff8cbe09d2 +https://conda.anaconda.org/conda-forge/linux-aarch64/json-c-0.17-h9d1147b_0.conda#d29aae0f70e8082d021842256c86b49f +https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.1-h4e544f5_0.tar.bz2#1f24853e59c68892452ef94ddd8afd4b +https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-h4de3ea5_0.tar.bz2#1a0ffc65e03ce81559dbcb0695ad1476 +https://conda.anaconda.org/conda-forge/linux-aarch64/libabseil-20230802.1-cxx17_h2f0025b_0.conda#d1d7afab9c131b52ffe11aed370d06cd +https://conda.anaconda.org/conda-forge/linux-aarch64/libaec-1.1.2-h2f0025b_1.conda#35cdc41045e1041d7f3bf29081b3d3cb +https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlicommon-1.1.0-h31becfc_1.conda#1b219fd801eddb7a94df5bd001053ad9 +https://conda.anaconda.org/conda-forge/linux-aarch64/libcrc32c-1.1.2-h01db608_0.tar.bz2#268ee639c17ada0002fb04dd21816cc2 +https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.19-h31becfc_0.conda#014e57e35f2dc95c9a12f63d4378e093 +https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda#a9a13cb143bbaa477b1ebaefbe47a302 +https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.5.0-hd600fc2_1.conda#6cd3d0a28437b3845c260f9d71d434d7 +https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.2-h3557bc0_5.tar.bz2#dddd85f4d52121fab0a8b099c5e06501 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-13.2.0-h582850c_5.conda#547486aac825d236de3beecb927b389c +https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.17-h31becfc_2.conda#9a8eb13f14de7d761555a98712e6df65 +https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.0.0-h31becfc_1.conda#ed24e702928be089d9ba3f05618515c6 +https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda#c14f32510f694e3185704d89967ec422 +https://conda.anaconda.org/conda-forge/linux-aarch64/libnuma-2.0.16-hb4cce97_1.conda#a63d3c8b8384e64056a8c4bfd80edbdd +https://conda.anaconda.org/conda-forge/linux-aarch64/libspatialindex-1.9.3-h01db608_4.tar.bz2#b68ef401a75f73e542f30851dc8eed49 +https://conda.anaconda.org/conda-forge/linux-aarch64/libutf8proc-2.8.0-h4e544f5_0.tar.bz2#bf0defbd8ac06270fb5ec05c85fb3c96 +https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda#000e30b09db0b7c775b21695dff30969 +https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.3.2-h31becfc_0.conda#1490de434d2a2c06a98af27641a2ffff +https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda#b4df5d7d4b63579d081fd3a4cf99740e +https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.2.13-h31becfc_5.conda#b213aa87eea9491ef7b129179322e955 +https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.9.4-hd600fc2_0.conda#500145a83ed07ce79c8cef24252f366b +https://conda.anaconda.org/conda-forge/linux-aarch64/lzo-2.10-h516909a_1000.tar.bz2#ef5661339990c399c68c71cfb341e6d7 +https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.4-h0425590_2.conda#4ff0a396150dedad4269e16e5810f769 +https://conda.anaconda.org/conda-forge/linux-aarch64/nspr-4.35-h4de3ea5_0.conda#7a392f26f76fc55354c8ed60c2b99162 +https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.2.1-h31becfc_0.conda#b7e7c53240214ae96f52a440c0b0126a +https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.43.2-h2f0025b_0.conda#896686714eea591ad0c0891800c571fd +https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-hb9de7d4_1001.tar.bz2#d0183ec6ce0b5aaa3486df25fa5f0ded +https://conda.anaconda.org/conda-forge/linux-aarch64/snappy-1.1.10-he8610fa_0.conda#11c25e55894bb8207a81a87e6a32b6e7 +https://conda.anaconda.org/conda-forge/linux-aarch64/tzcode-2024a-h31becfc_0.conda#d7691e522a386b757332784ee7f9906f +https://conda.anaconda.org/conda-forge/linux-aarch64/uriparser-0.9.7-hd600fc2_1.conda#72c24e7cfeeecdc48f0f8816b651796f +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-kbproto-1.0.7-h3557bc0_1002.tar.bz2#ec8ce6b3dac3945a4010559a6284b755 +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.1-h7935292_0.conda#025968e2637bca910b9b3e7f6743beff +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.11-h31becfc_0.conda#13de34f69cb73165dbe08c1e9148bedb +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.3-h3557bc0_0.tar.bz2#a6c9016ae1ca5c47a3603ed4cd65fedd +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-renderproto-0.11.1-h3557bc0_1002.tar.bz2#01cbfe96ce66b78a9a270ac305791dd2 +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xextproto-7.3.0-h2a766a3_1003.conda#32de1e4422c986e3b6eff59e7edc4d04 +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xproto-7.0.31-h3557bc0_1007.tar.bz2#987e98faa0ad2c667bbea6b6aae260bc +https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.2.6-h9cdd2b7_0.tar.bz2#83baad393a31d59c20b63ba4da6592df +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-cal-0.6.9-h854096e_3.conda#46e184887ee4385be10eeba7b958163c +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-compression-0.2.17-hf7cfaa6_8.conda#7d68a9481a4d4130a63751dc4ad9941b +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-sdkutils-0.1.14-hf7cfaa6_0.conda#dae4bd384e4e457dafae97b9329aa26f +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-checksums-0.1.17-hf7cfaa6_7.conda#ce2877af415607a91a37e53a60366a50 +https://conda.anaconda.org/conda-forge/linux-aarch64/expat-2.5.0-hd600fc2_1.conda#6dfca4be3e0080934b1105d009747e98 +https://conda.anaconda.org/conda-forge/linux-aarch64/glog-0.6.0-h8ab10f1_0.tar.bz2#9dc55595db8d7947bb253f63bbcec8ca +https://conda.anaconda.org/conda-forge/linux-aarch64/hdf4-4.2.15-hb6ba311_7.conda#e1b6676b77b9690d07ea25de48aed97e +https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlidec-1.1.0-h31becfc_1.conda#8db7cff89510bec0b863a0a8ee6a7bce +https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.1.0-h31becfc_1.conda#ad3d3a826b5848d99936e4466ebbaa26 +https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#29371161d77933a54fccf1bb66b96529 +https://conda.anaconda.org/conda-forge/linux-aarch64/libevent-2.1.12-h4ba1bb4_1.conda#96ae6083cd1ac9f6bc81631ac835b317 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-ng-13.2.0-he9431aa_5.conda#fab7c6a8c84492e18cbe578820e97a56 +https://conda.anaconda.org/conda-forge/linux-aarch64/libkml-1.3.0-h7d16752_1018.conda#0a2cb881ed5cf04e6e05079ee0a7a18b +https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.58.0-hb0e430d_1.conda#8f724cdddffa79152de61f5564a3526b +https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.42-h194ca79_0.conda#b8ff00cc9a5184726baea61244f8bec3 +https://conda.anaconda.org/conda-forge/linux-aarch64/libprotobuf-4.25.1-h87e877f_1.conda#c129f45da1472e472c28304046a92d9d +https://conda.anaconda.org/conda-forge/linux-aarch64/libre2-11-2023.06.02-hf48c5ca_0.conda#364a9630c8e1d565547c887e051e4c08 +https://conda.anaconda.org/conda-forge/linux-aarch64/librttopo-1.1.0-hd8968fb_15.conda#5df2305d559d0e956da65304bbaa9ba4 +https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.45.1-h194ca79_0.conda#4190198deb1ed253eb938f6a6d92ff4f +https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.0-h492db2e_0.conda#45532845e121677ad328c9af9953f161 +https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.15-h2a766a3_0.conda#eb3d8c8170e3d03f2564ed2024aa00c8 +https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.12.5-h3091e33_0.conda#2fcb5d64474a337f2a4213ec1dd40ce2 +https://conda.anaconda.org/conda-forge/linux-aarch64/libzip-1.10.1-h4156a30_3.conda#ad9400456170b46f2615bdd48dff87fe +https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.42-hd0f9c67_0.conda#683162253dd3b6c4d21bf037e59455f4 +https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8fc344f_1.conda#105eb1e16bf83bfb2eb380a48032b655 +https://conda.anaconda.org/conda-forge/linux-aarch64/s2n-1.4.3-h5a25046_0.conda#e5ef3389587af1374d830323ffdc007a +https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda#f75105e0585851f818e0009dd1dde4dc +https://conda.anaconda.org/conda-forge/linux-aarch64/ucx-1.15.0-hedb98eb_3.conda#6ac7b71587da701842bac2e3061a833e +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.4-h5a01bc2_0.conda#d788eca20ecd63bad8eea7219e5c5fb7 +https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.2.13-h31becfc_5.conda#96866c7301479abaf8308c50958c71a4 +https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.5-h4c53e97_0.conda#b74eb9dbb5c3c15cb3cee7cbdf198c75 +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-io-0.14.3-h5d4d345_1.conda#f5adc023af2640a0ca1be5cd4e4f131c +https://conda.anaconda.org/conda-forge/linux-aarch64/blosc-1.21.5-h2f3a684_0.conda#c1f53cf8a0e36464e084d9f167365552 +https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-bin-1.1.0-h31becfc_1.conda#9e4a13596ab651ea8d77aae023d0ce3f +https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.12.1-hf0a5ef3_2.conda#a5ab74c5bd158c3d5532b66d8d83d907 +https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.2-hc419048_0.conda#55b51af37bf6fdcfe06f140e62e8c8db +https://conda.anaconda.org/conda-forge/linux-aarch64/libarchive-3.7.2-hd2f85e0_1.conda#a0f2e7adbcdf4041d6ee273d07ca171e +https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.78.3-h311d5f7_0.conda#09e44253dee99895f02cfad44dd8fea4 +https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm15-15.0.7-hb4f23b0_4.conda#8d7aa8eae04dc19426a417528d7041eb +https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.26-pthreads_h5a5ec62_0.conda#2ea496754b596063335b3aeaa2b982ac +https://conda.anaconda.org/conda-forge/linux-aarch64/libthrift-0.19.0-h043aeee_1.conda#591ef1567ed4989d824fe35b45e3ae68 +https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.6.0-h1708d11_2.conda#d5638e110e7f22e2602a8edd20656720 +https://conda.anaconda.org/conda-forge/linux-aarch64/minizip-4.0.4-hb75dd74_0.conda#8bfd9232a180bae998793ebde11f8a77 +https://conda.anaconda.org/conda-forge/linux-aarch64/nss-3.97-hc5a5cc2_0.conda#69d61fcfcd726888d5c2add225461a35 +https://conda.anaconda.org/conda-forge/linux-aarch64/orc-1.9.2-h5960ff3_1.conda#33fba0519791e92eb6c5e807f82b9f63 +https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.12.1-h43d1f9e_1_cpython.conda#886aaa760e922b4f7e2522e2e0abd778 +https://conda.anaconda.org/conda-forge/linux-aarch64/re2-2023.06.02-h887e66c_0.conda#25adcadc54ca4932c6230f8da94d7c37 +https://conda.anaconda.org/conda-forge/linux-aarch64/sqlite-3.45.1-h3b3482f_0.conda#f48ce4824eeb9f8a383eb03e82e1ec77 +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.7-h055a233_0.conda#b3ff774afbb2cd7678044e1fed24f59e +https://conda.anaconda.org/conda-forge/noarch/attrs-23.2.0-pyh71513ae_0.conda#5e4c0743c70186509d1412e03c2d8dfa +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-event-stream-0.4.1-h96a4043_5.conda#835b55a424b3f10e5f6258b65efda9c0 +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-http-0.8.0-h28e27ac_5.conda#6d3738fcb9c1f5a62a3364aef23de578 +https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-1.1.0-h31becfc_1.conda#e41f5862ac746428407f3fd44d2ed01f +https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.1.0-py312h2aa54b4_1.conda#7253fd6feb797007a3d290bbcfd23a84 +https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda#0876280e409658fc6f9e75d035960333 +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda#7f4a9e3fcff3f6356ae99244a014da6a +https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda#f3ad426304898027fc619827ff428eca +https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 +https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.14.2-ha9a116f_0.conda#6d2d19ea85f9d41534cd28fdefd59a25 +https://conda.anaconda.org/conda-forge/linux-aarch64/freexl-2.0.0-h5428426_0.conda#1338ecf4f6072e376e87f3ae6bc34170 +https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda#1a76f09108576397c41c0b0c5bd84134 +https://conda.anaconda.org/conda-forge/linux-aarch64/kiwisolver-1.4.5-py312h721a97f_1.conda#3f53f74e99a5fa60e390e139bfe2ef5f +https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.16-h922389a_0.conda#ffdd8267a04c515e7ce69c727b051414 +https://conda.anaconda.org/conda-forge/linux-aarch64/libblas-3.9.0-21_linuxaarch64_openblas.conda#7358230781e5d6e76e6adacf5201bcdf +https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.5.0-h4e8248e_0.conda#fa0f5edc06ffc25a01eed005c6dc3d8c +https://conda.anaconda.org/conda-forge/linux-aarch64/libgrpc-1.60.1-heeb7df3_0.conda#e3c79b6da73add66a7b61b351b047c83 +https://conda.anaconda.org/conda-forge/linux-aarch64/libpq-16.2-h58720eb_0.conda#ca0572c11209701741812f657ebeef82 +https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-2.1.5-py312h9ef2f89_0.conda#a00135adde3dfe19b9962c3c767c2129 +https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 +https://conda.anaconda.org/conda-forge/noarch/networkx-3.2.1-pyhd8ed1ab_0.conda#425fce3b531bed6ec3c74fab3e5f0a1c +https://conda.anaconda.org/conda-forge/linux-aarch64/openjpeg-2.5.0-h0d9d63b_3.conda#123f5df3bc7f0e23c6950fddb97d1f43 +https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda#79002079284aa895f883c6b7f3f88fd6 +https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.1-pyhd8ed1ab_0.conda#176f7d56f0cfe9008bdf1bccd7de02fb +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda#98206ea9954216ee7540f0c773f2104d +https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda#3eeeeb9e4827ace8c0c1419c85d590ad +https://conda.anaconda.org/conda-forge/linux-aarch64/rtree-1.2.0-py312h3b32434_0.conda#e3510a703f11a062a3b3640b8ac3b8d8 +https://conda.anaconda.org/conda-forge/noarch/setuptools-69.0.3-pyhd8ed1ab_0.conda#40695fdfd15a92121ed2922900d0308b +https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.2.0-pyha21a80b_0.conda#978d03388b62173b8e6f79162cf52b86 +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.4-h2a766a3_2.conda#0cea7d840c8eeaa4e349e0b4775c826d +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.11-h7935292_0.conda#8c96b84f7fb97a3cd533a14dbdcd6626 +https://conda.anaconda.org/conda-forge/noarch/xyzservices-2023.10.1-pyhd8ed1ab_0.conda#1e0d85c0e2fef9539218da185b285f54 +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-auth-0.7.15-hd33d976_0.conda#62dcef13e6af70a3b62c984aa64243de +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-mqtt-0.10.1-h54b3c4e_3.conda#6a213b976295ad8bdfd4be8c8cff2e4c +https://conda.anaconda.org/conda-forge/linux-aarch64/azure-core-cpp-1.10.3-hcd87347_1.conda#6d52760d398c383340c645b676d6d7c8 +https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.0-ha13f110_0.conda#425111f8cc6945c5d1307357dd819b9b +https://conda.anaconda.org/conda-forge/linux-aarch64/cfitsio-4.3.1-hf28c5f1_0.conda#3b1ede3e444833dbd1f6ac717ae5dfb3 +https://conda.anaconda.org/conda-forge/noarch/click-plugins-1.1.1-py_0.tar.bz2#4fd2c6b53934bd7d96d1f3fdaf99b79f +https://conda.anaconda.org/conda-forge/noarch/cligj-0.7.2-pyhd8ed1ab_1.tar.bz2#a29b7c141d6b2de4bb67788a5f107734 +https://conda.anaconda.org/conda-forge/linux-aarch64/fonttools-4.48.1-py312hdd3e373_0.conda#cb96e2a14e0296f2704dd55ad3627855 +https://conda.anaconda.org/conda-forge/linux-aarch64/hdf5-1.14.3-nompi_ha486f32_100.conda#87cd6b1683c0eea2ba2856700aeee2cf +https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 +https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda#4da50d410f553db77e62ab62ffaa1abc +https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.9.0-21_linuxaarch64_openblas.conda#7eb9aa7a90f067f8dbfede586cdc55cd +https://conda.anaconda.org/conda-forge/linux-aarch64/libgoogle-cloud-2.12.0-h3b99733_5.conda#78da954aaa5fb664f2035215d5091a5b +https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.9.0-21_linuxaarch64_openblas.conda#ab08b651e3630c20d3032e59859f34f7 +https://conda.anaconda.org/conda-forge/linux-aarch64/pillow-10.2.0-py312h1e2a6dd_0.conda#f5556f9618687237ead7b55de1bcd80c +https://conda.anaconda.org/conda-forge/linux-aarch64/postgresql-16.2-he703394_0.conda#7d74ee5fdce72a81c2f2a31bb614a561 +https://conda.anaconda.org/conda-forge/linux-aarch64/proj-9.3.1-h7b42f86_0.conda#fa6ab94a4d428b968daf32cd556fea81 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2#dd999d1cc9f79e67dbb855c8924c7984 +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.0-pyhd8ed1ab_0.conda#6a7e0694921f668a030d52f0c47baebd +https://conda.anaconda.org/conda-forge/linux-aarch64/xerces-c-3.2.5-hf13c1fb_0.conda#5c6a84e179f9fc7f8e0890c28704a8ce +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-s3-0.5.0-hc48d4bd_2.conda#2ea39090e4f2c2d37f0e7ec7f8169015 +https://conda.anaconda.org/conda-forge/linux-aarch64/azure-storage-common-cpp-12.5.0-hee0c750_2.conda#99479410b3dcf4b5260a692a94f9d46b +https://conda.anaconda.org/conda-forge/noarch/branca-0.7.1-pyhd8ed1ab_0.conda#35fa1bfd27c4d4c3cd46501a9ca7bd78 +https://conda.anaconda.org/conda-forge/linux-aarch64/geotiff-1.7.1-h3e58e51_15.conda#11964a0f5b8b311ee1f5944f0e9a6c37 +https://conda.anaconda.org/conda-forge/linux-aarch64/kealib-1.5.3-h4670d8b_0.conda#f1949c81436aab570e9ce976697566bf +https://conda.anaconda.org/conda-forge/linux-aarch64/libnetcdf-4.9.2-nompi_h33102a8_113.conda#ea31ab8d4f2168dd9e9f5d15ede2bb48 +https://conda.anaconda.org/conda-forge/linux-aarch64/libspatialite-5.1.0-h896d346_4.conda#7333624f70da943d90ef933c22c04c76 +https://conda.anaconda.org/conda-forge/linux-aarch64/numpy-1.26.4-py312h470d778_0.conda#9cebf5a06cb87d4569cd68df887af476 +https://conda.anaconda.org/conda-forge/linux-aarch64/poppler-24.02.0-h3cd87ed_0.conda#6e55400bd072c8adfb30cf3891a0eb3d +https://conda.anaconda.org/conda-forge/linux-aarch64/pyproj-3.6.1-py312hb621bc5_5.conda#e43cd567d337c51b83ecc46364971a90 +https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-crt-cpp-0.26.1-hd9c1a88_9.conda#af49defa34c431a29631a34c4915a378 +https://conda.anaconda.org/conda-forge/linux-aarch64/azure-storage-blobs-cpp-12.10.0-h2a328a1_0.conda#421b4ee5509aba7be1be6785444f6278 +https://conda.anaconda.org/conda-forge/linux-aarch64/contourpy-1.2.0-py312h8f0b210_0.conda#4a5ce41c9540a44e79855a33787a1da6 +https://conda.anaconda.org/conda-forge/noarch/folium-0.15.1-pyhd8ed1ab_0.conda#4fdfc338f6fb875b1078a7e20dbc99e2 +https://conda.anaconda.org/conda-forge/linux-aarch64/pandas-2.2.0-py312hc56aa73_0.conda#6bd70fe15450544d7ab4fc2a546da7e1 +https://conda.anaconda.org/conda-forge/linux-aarch64/scipy-1.12.0-py312h470d778_2.conda#368ed86b1c7790f56228d2aeb98fcc29 +https://conda.anaconda.org/conda-forge/linux-aarch64/shapely-2.0.2-py312h15622cc_1.conda#8c8d051410bc0c92c6756d3dbf7252d5 +https://conda.anaconda.org/conda-forge/linux-aarch64/aws-sdk-cpp-1.11.242-hf01a265_0.conda#f930fb3e674034eb24eddf591449eea7 +https://conda.anaconda.org/conda-forge/noarch/geopandas-base-0.14.3-pyha770c72_0.conda#fbac4b2194c962b97324a3f5dd7d2696 +https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-base-3.8.2-py312h132ec79_0.conda#0d505c874fc7a33b13669d5222465e02 +https://conda.anaconda.org/conda-forge/linux-aarch64/scikit-learn-1.4.0-py312ha513981_0.conda#00f272e7d6be0dab0da6e670fc254910 +https://conda.anaconda.org/conda-forge/linux-aarch64/tiledb-2.19.1-hf61e980_0.conda#5b3a763d5471858249ba058cd137876a +https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-15.0.0-h94a09b9_2_cpu.conda#ffb8a09d7c25b8239e3f06e2edbfec06 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgdal-3.8.3-h4c8926b_2.conda#b5b3807eb7445030c97075d7026490ea +https://conda.anaconda.org/conda-forge/noarch/mapclassify-2.6.1-pyhd8ed1ab_0.conda#6aceae1ad4f16cf7b73ee04189947f98 +https://conda.anaconda.org/conda-forge/linux-aarch64/gdal-3.8.3-py312hd8c316d_2.conda#c85b49ea2e37b3ceb27f6cd93205d62c +https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-acero-15.0.0-h2f0025b_2_cpu.conda#3ef071eab02a57d922b1dfb45e31e503 +https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-flight-15.0.0-he69d72d_2_cpu.conda#72af0cdd7747e59d32f8159526d1fb0d +https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-gandiva-15.0.0-h1bc7839_2_cpu.conda#0a96e3c0088ad1d4a7e84de68b59133f +https://conda.anaconda.org/conda-forge/linux-aarch64/libparquet-15.0.0-hb18b541_2_cpu.conda#9813a3ebd65bb708b1803b055bd5f556 +https://conda.anaconda.org/conda-forge/linux-aarch64/fiona-1.9.5-py312h78df3c0_3.conda#41fd7111938baea47ecfb1dbd7be7cf6 +https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-dataset-15.0.0-h2f0025b_2_cpu.conda#2a6e6d05bad4329ac1a663a26b64782d +https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-flight-sql-15.0.0-h1fc705f_2_cpu.conda#e749637fe8eb97f1ecc145b33fc1981b +https://conda.anaconda.org/conda-forge/noarch/geopandas-0.14.3-pyhd8ed1ab_0.conda#d8e208e375441bf1404e9693f13f3c25 +https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-substrait-15.0.0-h0599332_2_cpu.conda#3b09bcb42a4012cfd9c83b6b185f16af +https://conda.anaconda.org/conda-forge/linux-aarch64/pyarrow-15.0.0-py312h2f65cca_2_cpu.conda#3509913255b9118b674a0d1635cf3f44 diff --git a/targets/python-container/environment.yml b/targets/python-container/environment.yml new file mode 100644 index 000000000..fda529b9f --- /dev/null +++ b/targets/python-container/environment.yml @@ -0,0 +1,11 @@ +name: sliderule-container-runtime-environment +channels: + - conda-forge + - nodefaults +dependencies: + - numpy + - requests + - fiona + - geopandas + - shapely + - pyarrow diff --git a/targets/python-container/helloworld.py b/targets/python-container/helloworld.py new file mode 100755 index 000000000..b52660b87 --- /dev/null +++ b/targets/python-container/helloworld.py @@ -0,0 +1,3 @@ +# python + +print("Hello World") diff --git a/targets/slideruleearth-aws/Makefile b/targets/slideruleearth-aws/Makefile index 3e4fc20b3..cbe4c32b5 100644 --- a/targets/slideruleearth-aws/Makefile +++ b/targets/slideruleearth-aws/Makefile @@ -14,6 +14,8 @@ # The binaries are sufficient, but pay close attention to the local package versions # * Node.js # The javascript npm package for SlideRule is updated via a node.js script on release +# * conda-lock +# The Python base image used for the container runtime environment uses conda-lock to create the conda dependency file # # To release a version of SlideRule: # 1. Update .aws/credentials file with a temporary session token; {profile} references your long term aws credentials, {username} is your aws account, {code} is your MFA token @@ -60,6 +62,7 @@ PROXY_STAGE_DIR = $(STAGE)/proxy TF_STAGE_DIR = $(STAGE)/tf STATIC_WEB_SOURCE_DIR = $(ROOT)/docs STATIC_WEB_STAGE_DIR = $(STAGE)/website +PYTHON_CONTAINER_STAGE_DIR = $(STAGE)/container-runtime-python INSTALL_DIR ?= $(SERVER_STAGE_DIR) VERSION ?= latest @@ -187,6 +190,21 @@ manager-destroy: ## destroy manager using terraform; needs DOMAIN cd terraform/manager && terraform workspace select $(DOMAIN)-manager|| terraform workspace new $(DOMAIN)-manager cd terraform/manager && terraform destroy +container-runtime-python-lock: ## create the lock file for the container runtime environment base python image +# cd ../python-container && conda-lock -p linux-aarch64 -f environment.yml + cd ../python-container && conda-lock render -p linux-aarch64 + +container-runtime-python-docker: ## create the container runtime environment base python image + -rm -Rf $(PYTHON_CONTAINER_STAGE_DIR) + mkdir -p $(PYTHON_CONTAINER_STAGE_DIR) + cp ../python-container/* $(PYTHON_CONTAINER_STAGE_DIR) + cd $(PYTHON_CONTAINER_STAGE_DIR) && docker $(BUILDX) build $(DOCKEROPTS) -t $(ECR)/python-container:latest $(DOCKER_PLATFORM) . + docker tag $(ECR)/python-container:latest $(ECR)/python-container:$(VERSION) + docker tag $(ECR)/python-container:latest $(ECR)/python-container:$(MAJOR_VERSION) + +container-runtime-python-run: ## run the build environment docker container + docker run -it -v $(ROOT):/host --rm --name cre $(ECR)/python-container:$(VERSION) python /host/targets/python-container/helloworld.py + static-website-docker: ## make the static website docker image; needs VERSION -rm -Rf $(STATIC_WEB_STAGE_DIR) mkdir -p $(STATIC_WEB_STAGE_DIR) diff --git a/targets/slideruleearth-aws/docker-compose.yml b/targets/slideruleearth-aws/docker-compose.yml index f44bfbaba..c09b504ea 100644 --- a/targets/slideruleearth-aws/docker-compose.yml +++ b/targets/slideruleearth-aws/docker-compose.yml @@ -20,6 +20,7 @@ services: - ORCHESTRATOR=http://127.0.0.1:8050 - CLUSTER=sliderule - PROVISIONING_SYSTEM=https://ps.localhost + - CONTAINER_REGISTRY=742127912612.dkr.ecr.us-west-2.amazonaws.com - ENVIRONMENT_VERSION=$ENVIRONMENT_VERSION healthcheck: test: curl -X GET -d "{}" http://localhost:10081/probe/health || exit 1 diff --git a/targets/slideruleearth-aws/terraform/cluster/docker-compose-sliderule.yml b/targets/slideruleearth-aws/terraform/cluster/docker-compose-sliderule.yml index bca450d99..16d7a251b 100644 --- a/targets/slideruleearth-aws/terraform/cluster/docker-compose-sliderule.yml +++ b/targets/slideruleearth-aws/terraform/cluster/docker-compose-sliderule.yml @@ -20,6 +20,7 @@ services: - ORCHESTRATOR=http://10.0.1.5:8050 - CLUSTER=$CLUSTER - PROVISIONING_SYSTEM=$PROVISIONING_SYSTEM + - CONTAINER_REGISTRY=$CONTAINER_REGISTRY - ENVIRONMENT_VERSION=$ENVIRONMENT_VERSION healthcheck: test: curl -X GET -d "{}" http://localhost:10081/probe/health || exit 1 diff --git a/targets/slideruleearth-aws/terraform/cluster/sliderule-asg.tf b/targets/slideruleearth-aws/terraform/cluster/sliderule-asg.tf index 988b12645..cc7fba69b 100644 --- a/targets/slideruleearth-aws/terraform/cluster/sliderule-asg.tf +++ b/targets/slideruleearth-aws/terraform/cluster/sliderule-asg.tf @@ -57,6 +57,7 @@ resource "aws_launch_configuration" "sliderule-instance" { export CLUSTER=${var.cluster_name} export SLIDERULE_IMAGE=${var.container_repo}/sliderule:${var.cluster_version} export PROVISIONING_SYSTEM="https://ps.${var.domain}" + export CONTAINER_REGISTRY=${var.container_repo} aws s3 cp s3://sliderule/infrastructure/software/${var.cluster_name}-docker-compose-sliderule.yml ./docker-compose.yml docker-compose -p cluster up --detach EOF From 8954add50ee86b6e2157b49584e9e58916275513 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Tue, 13 Feb 2024 22:01:07 +0000 Subject: [PATCH 05/43] first signs of running the cre endpoint --- clients/python/sliderule/container.py | 71 + clients/python/utils/test.py | 5 + packages/core/EndpointObject.h | 1 + packages/cre/CMakeLists.txt | 6 +- packages/cre/ContainerRunner.cpp | 86 +- packages/cre/ContainerRunner.h | 9 +- packages/cre/CreParms.cpp | 27 +- packages/cre/CreParms.h | 2 + packages/netsvc/ProvisioningSystemLib.cpp | 1 - scripts/endpoints/cre.lua | 15 +- .../python-container/conda-linux-aarch64.lock | 2 +- targets/python-container/conda-lock.yml | 3062 +++++++++++++++++ targets/slideruleearth-aws/Makefile | 2 +- targets/slideruleearth-aws/config.json | 3 +- 14 files changed, 3245 insertions(+), 47 deletions(-) create mode 100644 clients/python/sliderule/container.py create mode 100644 clients/python/utils/test.py create mode 100644 targets/python-container/conda-lock.yml diff --git a/clients/python/sliderule/container.py b/clients/python/sliderule/container.py new file mode 100644 index 000000000..b208209a5 --- /dev/null +++ b/clients/python/sliderule/container.py @@ -0,0 +1,71 @@ +# Copyright (c) 2021, University of Washington +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the University of Washington nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF WASHINGTON AND CONTRIBUTORS +# “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF WASHINGTON OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import time +import logging +import sliderule +from sliderule import logger + +############################################################################### +# GLOBALS +############################################################################### + +# profiling times for each major function +profiles = {} + +############################################################################### +# LOCAL FUNCTIONS +############################################################################### + +############################################################################### +# APIs +############################################################################### + + +# +# Execute Container +# +def execute (parm): + ''' + Executes a containerized application using SlideRule + + Parameters + ---------- + parms: dict + parameters used to configure container runtime environment + + Returns + ------- + str + json response + ''' + tstart = time.perf_counter() + rsps = sliderule.source("cre", {"parms": parm}) + profiles[execute.__name__] = time.perf_counter() - tstart + return rsps diff --git a/clients/python/utils/test.py b/clients/python/utils/test.py new file mode 100644 index 000000000..581d098e0 --- /dev/null +++ b/clients/python/utils/test.py @@ -0,0 +1,5 @@ +from sliderule import sliderule, container +sliderule.init("localhost", organization=None) +parms = {"image": "python-container", "script": "helloworld.py"} +rsps = container.execute(parms) +print(rsps) \ No newline at end of file diff --git a/packages/core/EndpointObject.h b/packages/core/EndpointObject.h index fc001c2cc..a7f3cfbbc 100644 --- a/packages/core/EndpointObject.h +++ b/packages/core/EndpointObject.h @@ -76,6 +76,7 @@ class EndpointObject: public LuaObject typedef enum { OK = 200, + Created = 201, Bad_Request = 400, Unauthorized = 401, Not_Found = 404, diff --git a/packages/cre/CMakeLists.txt b/packages/cre/CMakeLists.txt index 0f8b0608b..bc4c50304 100644 --- a/packages/cre/CMakeLists.txt +++ b/packages/cre/CMakeLists.txt @@ -1,8 +1,11 @@ # Find cURL Library find_package (CURL) +# Find RapidJSON Library +find_package (RapidJSON) + # Build package -if (CURL_FOUND) +if (CURL_FOUND AND RapidJSON_FOUND) message (STATUS "Including cre package") @@ -10,6 +13,7 @@ if (CURL_FOUND) target_link_libraries (slideruleLib PUBLIC ${CURL_LIBRARIES}) target_include_directories (slideruleLib PUBLIC ${CURL_INCLUDE_DIR}) + target_include_directories (slideruleLib PUBLIC ${RapidJSON_INCLUDE_DIR}) target_sources(slideruleLib PRIVATE diff --git a/packages/cre/ContainerRunner.cpp b/packages/cre/ContainerRunner.cpp index 5447c1af7..208c47e81 100644 --- a/packages/cre/ContainerRunner.cpp +++ b/packages/cre/ContainerRunner.cpp @@ -37,6 +37,7 @@ #include "OsApi.h" #include "CurlLib.h" // netsvc package dependency #include "EndpointObject.h" +#include /****************************************************************************** * STATIC DATA @@ -67,7 +68,13 @@ int ContainerRunner::luaCreate (lua_State* L) /* Get Parameters */ _parms = dynamic_cast(getLuaObject(L, 1, CreParms::OBJECT_TYPE)); - /* Create Dispatch */ + /* Check Environment */ + if(REGISTRY == NULL) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "container registry must be set before a container can be run"); + } + + /* Create Container Runner */ return createLuaObject(L, new ContainerRunner(L, _parms)); } catch(const RunTimeException& e) @@ -107,11 +114,13 @@ const char* ContainerRunner::getRegistry (void) /*---------------------------------------------------------------------------- * Constructor *----------------------------------------------------------------------------*/ -ContainerRunner::ContainerRunner (lua_State* L, const CreParms* _parms): +ContainerRunner::ContainerRunner (lua_State* L, CreParms* _parms): LuaObject(L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), - parms(_parms), result(NULL) { + assert(_parms); + + parms = _parms; active = true; controlPid = new Thread(controlThread, this); } @@ -123,7 +132,8 @@ ContainerRunner::~ContainerRunner (void) { active = false; delete controlPid; - delete result; + delete [] result; + parms->releaseLuaObject(); } /*---------------------------------------------------------------------------- @@ -131,30 +141,62 @@ ContainerRunner::~ContainerRunner (void) *----------------------------------------------------------------------------*/ void* ContainerRunner::controlThread (void* parm) { - ContainerRunner* cr = static_cast(parm); - const char* result = NULL; + ContainerRunner* cr = reinterpret_cast(parm); + const char* result = StringLib::duplicate("{\"result\": \"testing...\"}"); - /* Run Container */ + /* Set Docker Socket */ const char* unix_socket = "/var/run/docker.sock"; - const char* url= "http://localhost/v1.44/containers/create"; - FString data("{\"Image\": \"%s/%s\"}", REGISTRY, cr->parms->image); + const char* api_version = "v1.43"; + + /* Configure HTTP Headers */ List headers(5); string* content_type = new string("Content-Type: application/json"); headers.add(content_type); - const char* response = NULL; - long http_code = CurlLib::request(EndpointObject::POST, url, data.c_str(), &response, NULL, false, false, &headers, unix_socket); - if(http_code != EndpointObject::OK) mlog(CRITICAL, "Failed to start container <%s>: %s", cr->parms->image, response); - delete [] response; - - /* Wait for Completion and Get Result */ - if(http_code == EndpointObject::OK) - { - /* Poll Completion of Container */ - // using timeout - /* Get Result */ - // from well known file - } + /* Build Container Parameters */ + FString image("\"Image\": \"%s/%s\"", REGISTRY, cr->parms->image); + FString host_config("\"HostConfig\": { \"Binds\": [\"%s:%s\"] }", "/usr/local/share/applications", "/applications"); + FString cmd("\"Cmd\": [\"python\", \"%s\"]}", cr->parms->script); + FString data("{%s, %s, %s}", image.c_str(), host_config.c_str(), cmd.c_str()); + + /* Create Container */ + FString create_url("http://localhost/%s/containers/create", api_version); + const char* create_response = NULL; + long create_http_code = CurlLib::request(EndpointObject::POST, create_url.c_str(), data.c_str(), &create_response, NULL, false, false, &headers, unix_socket); + if(create_http_code != EndpointObject::Created) mlog(CRITICAL, "Failed to create container <%s>: %ld - %s", cr->parms->image, create_http_code, create_response); + else mlog(INFO, "Created container <%s>: %s", cr->parms->image, create_response); +// +// /* Wait for Completion and Get Result */ +// if(false && create_http_code == EndpointObject::OK) +// { +// /* Get Container ID */ +// rapidjson::Document json; +// json.Parse(create_response); +// const char* container_id = json["Id"].GetString(); +// +// /* Start Container */ +// FString start_url("http://localhost/%s/containers/%s/start", api_version, container_id); +// const char* start_response = NULL; +// long start_http_code = CurlLib::request(EndpointObject::POST, start_url.c_str(), NULL, &start_response, NULL, false, false, NULL, unix_socket); +// if(start_http_code != EndpointObject::OK) mlog(CRITICAL, "Failed to start container <%s>: %s", cr->parms->image, start_response); +// +// /* Poll Completion of Container */ +// FString wait_url("http://localhost/%s/containers/%s/wait", api_version, container_id); +// const char* wait_response = NULL; +// long wait_http_code = CurlLib::request(EndpointObject::POST, wait_url.c_str(), NULL, &wait_response, NULL, false, false, NULL, unix_socket); +// if(wait_http_code != EndpointObject::OK) mlog(CRITICAL, "Failed to wait for container <%s>: %s", cr->parms->image, wait_response); +// // TODO - need to poll somehow and tie in the timeout +// +// /* Get Result */ +// // from well known file +// +// /* Clean Up */ +// delete [] start_response; +// delete [] wait_response; +// } +// + /* Clean Up */ + delete [] create_response; /* Signal Complete */ cr->resultLock.lock(); diff --git a/packages/cre/ContainerRunner.h b/packages/cre/ContainerRunner.h index 8b97c48e3..dc05145b9 100644 --- a/packages/cre/ContainerRunner.h +++ b/packages/cre/ContainerRunner.h @@ -59,12 +59,6 @@ class ContainerRunner: public LuaObject static const int RESULT_SIGNAL = 0; static const int DEFAULT_TIMEOUT = 600; - /*-------------------------------------------------------------------- - * Data - *--------------------------------------------------------------------*/ - - const CreParms* parms; - /*-------------------------------------------------------------------- * Methods *--------------------------------------------------------------------*/ @@ -88,12 +82,13 @@ class ContainerRunner: public LuaObject Thread* controlPid; const char* result; Cond resultLock; + CreParms* parms; /*-------------------------------------------------------------------- * Methods *--------------------------------------------------------------------*/ - ContainerRunner (lua_State* L, const CreParms* _parms); + ContainerRunner (lua_State* L, CreParms* _parms); virtual ~ContainerRunner (void); static void* controlThread (void* parm); diff --git a/packages/cre/CreParms.cpp b/packages/cre/CreParms.cpp index 5388cfe79..fcbf337c5 100644 --- a/packages/cre/CreParms.cpp +++ b/packages/cre/CreParms.cpp @@ -42,6 +42,7 @@ const char* CreParms::SELF = "output"; const char* CreParms::IMAGE = "image"; +const char* CreParms::SCRIPT = "script"; const char* CreParms::TIMEOUT = "timeout"; const char* CreParms::OBJECT_TYPE = "CreParms"; @@ -84,6 +85,7 @@ int CreParms::luaCreate (lua_State* L) CreParms::CreParms (lua_State* L, int index): LuaObject (L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), image (NULL), + script (NULL), timeout (DEFAULT_TIMEOUT) { /* Populate Object from Lua */ @@ -96,27 +98,30 @@ CreParms::CreParms (lua_State* L, int index): /* Image */ lua_getfield(L, index, IMAGE); - const char* _image = StringLib::duplicate(LuaObject::getLuaString(L, -1, true, image, &field_provided)); + image = StringLib::duplicate(LuaObject::getLuaString(L, -1, true, image, &field_provided)); if(field_provided) { + mlog(DEBUG, "Setting %s to %s", IMAGE, image); + /* Check Image for ONLY Legal Characters */ - string s(_image); + string s(image); for (auto c_iter = s.begin(); c_iter < s.end(); ++c_iter) { int c = *c_iter; - if(!isalnum(c) && (c != '/') && (c != '.') && (c != '-')) + if(!isalnum(c) && (c != '/') && (c != '.') && (c != ':') && (c != '-')) { - delete [] _image; throw RunTimeException(CRITICAL, RTE_ERROR, "invalid character found in image name: %c", c); } } - - /* Set Image */ - image = _image; - mlog(DEBUG, "Setting %s to %s", IMAGE, image); } lua_pop(L, 1); + /* Script */ + lua_getfield(L, index, SCRIPT); + script = StringLib::duplicate(LuaObject::getLuaString(L, -1, true, script, &field_provided)); + if(field_provided) mlog(DEBUG, "Setting %s to %s", SCRIPT, script); + lua_pop(L, 1); + /* Timeout */ lua_getfield(L, index, TIMEOUT); timeout = LuaObject::getLuaInteger(L, -1, true, timeout, &field_provided); @@ -149,6 +154,12 @@ void CreParms::cleanup (void) delete [] image; image = NULL; } + + if(script) + { + delete [] script; + script = NULL; + } } /*---------------------------------------------------------------------------- diff --git a/packages/cre/CreParms.h b/packages/cre/CreParms.h index 8321bfa21..b62797b1b 100644 --- a/packages/cre/CreParms.h +++ b/packages/cre/CreParms.h @@ -53,6 +53,7 @@ class CreParms: public LuaObject static const char* SELF; static const char* IMAGE; + static const char* SCRIPT; static const char* TIMEOUT; static const char* OBJECT_TYPE; @@ -66,6 +67,7 @@ class CreParms: public LuaObject *--------------------------------------------------------------------*/ const char* image; // container image + const char* script; // python file int timeout; /*-------------------------------------------------------------------- diff --git a/packages/netsvc/ProvisioningSystemLib.cpp b/packages/netsvc/ProvisioningSystemLib.cpp index 5244d33bc..261ae6461 100644 --- a/packages/netsvc/ProvisioningSystemLib.cpp +++ b/packages/netsvc/ProvisioningSystemLib.cpp @@ -38,7 +38,6 @@ #include "core.h" #include -#include /****************************************************************************** * PROVISIONING SYSTEM LIBRARY CLASS diff --git a/scripts/endpoints/cre.lua b/scripts/endpoints/cre.lua index 9fe762fef..ac9b42f89 100644 --- a/scripts/endpoints/cre.lua +++ b/scripts/endpoints/cre.lua @@ -3,17 +3,22 @@ -- -- INPUT: arg[1] -- { --- "image": "" --- "timeout": +-- "parms": +-- { +-- "image": "", +-- "timeout": , +-- "userdata": "" +-- } -- } -- -- OUTPUT: -- -local json = require("json") -local parm = json.decode(arg[1]) +local json = require("json") +local rqst = json.decode(arg[1]) +local parms = rqst["parms"] -local cre_parms = cre.parms(parm) +local cre_parms = cre.parms(parms) local cre_runner = cre.container(cre_parms) local result = cre_runner:result() diff --git a/targets/python-container/conda-linux-aarch64.lock b/targets/python-container/conda-linux-aarch64.lock index dfa20d2ab..f0b69a2c1 100644 --- a/targets/python-container/conda-linux-aarch64.lock +++ b/targets/python-container/conda-linux-aarch64.lock @@ -8,7 +8,7 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed3 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda#6185f640c43843e5ad6fd1c5372c3f80 https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.40-h2d8c526_0.conda#16246d69e945d0b1969a6099e7c5d457 -https://conda.anaconda.org/conda-forge/linux-aarch64/libboost-headers-1.84.0-h8af1aa0_0.conda#6ee13ea4adf1595a70917ef0dc62d876 +https://conda.anaconda.org/conda-forge/linux-aarch64/libboost-headers-1.84.0-h8af1aa0_1.conda#e74d7ab5b78bf73c779961cd124d19f9 https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-13.2.0-hf8544c7_5.conda#379be2f115ffb73860e4e260dd2170b7 https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-13.2.0-h9a76618_5.conda#1b79d37dce0fad96bdf3de03925f43b4 https://conda.anaconda.org/conda-forge/noarch/poppler-data-0.4.12-hd8ed1ab_0.conda#d8d7293c5b37f39b2ac32940621c6592 diff --git a/targets/python-container/conda-lock.yml b/targets/python-container/conda-lock.yml new file mode 100644 index 000000000..f72b8883f --- /dev/null +++ b/targets/python-container/conda-lock.yml @@ -0,0 +1,3062 @@ +# This lock file was generated by conda-lock (https://github.com/conda/conda-lock). DO NOT EDIT! +# +# A "lock file" contains a concrete list of package versions (with checksums) to be installed. Unlike +# e.g. `conda env create`, the resulting environment will not change as new package versions become +# available, unless you explicitly update the lock file. +# +# Install this environment as "YOURENV" with: +# conda-lock install -n YOURENV --file conda-lock.yml +# To update a single package to the latest version compatible with the version constraints in the source: +# conda-lock lock --lockfile conda-lock.yml --update PACKAGE +# To re-solve the entire environment, e.g. after changing a version constraint in the source file: +# conda-lock -f environment.yml --lockfile conda-lock.yml +version: 1 +metadata: + content_hash: + linux-aarch64: b1f118743ac6af4d4e472d90afbc78d1bc0cac209f8bfa143bd66ff3ac702532 + channels: + - url: conda-forge + used_env_vars: [] + platforms: + - linux-aarch64 + sources: + - environment.yml +package: +- name: _openmp_mutex + version: '4.5' + manager: conda + platform: linux-aarch64 + dependencies: + libgomp: '>=7.5.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 + hash: + md5: 6168d71addc746e8f2b8d57dfd2edcea + sha256: 3702bef2f0a4d38bd8288bbe54aace623602a1343c2cfbefd3fa188e015bebf0 + category: main + optional: false +- name: attrs + version: 23.2.0 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/attrs-23.2.0-pyh71513ae_0.conda + hash: + md5: 5e4c0743c70186509d1412e03c2d8dfa + sha256: 77c7d03bdb243a048fff398cedc74327b7dc79169ebe3b4c8448b0331ea55fea + category: main + optional: false +- name: aws-c-auth + version: 0.7.15 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-cal: '>=0.6.9,<0.6.10.0a0' + aws-c-common: '>=0.9.12,<0.9.13.0a0' + aws-c-http: '>=0.8.0,<0.8.1.0a0' + aws-c-io: '>=0.14.3,<0.14.4.0a0' + aws-c-sdkutils: '>=0.1.14,<0.1.15.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-auth-0.7.15-hd33d976_0.conda + hash: + md5: 62dcef13e6af70a3b62c984aa64243de + sha256: 5e666090e5260b3dad2d5271defb76eab45d8fa47dcdb7f86b53f499becf2998 + category: main + optional: false +- name: aws-c-cal + version: 0.6.9 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-common: '>=0.9.12,<0.9.13.0a0' + libgcc-ng: '>=12' + openssl: '>=3.2.0,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-cal-0.6.9-h854096e_3.conda + hash: + md5: 46e184887ee4385be10eeba7b958163c + sha256: 1c7363ba8cb40f67732c530c49c983393cfb1b09325d599d89c6f696981271a3 + category: main + optional: false +- name: aws-c-common + version: 0.9.12 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-common-0.9.12-h31becfc_0.conda + hash: + md5: 6f917de3433c28ef387d1b2df5f6a624 + sha256: d02b816414bdaa1b137c427f0403ca6e288d63f77530d44a6bce16a0d4b792c2 + category: main + optional: false +- name: aws-c-compression + version: 0.2.17 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-common: '>=0.9.12,<0.9.13.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-compression-0.2.17-hf7cfaa6_8.conda + hash: + md5: 7d68a9481a4d4130a63751dc4ad9941b + sha256: 58dc396e056b317a60ae948b91ed794bdc4081236db8e196e688002c49542ae4 + category: main + optional: false +- name: aws-c-event-stream + version: 0.4.1 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-common: '>=0.9.12,<0.9.13.0a0' + aws-c-io: '>=0.14.3,<0.14.4.0a0' + aws-checksums: '>=0.1.17,<0.1.18.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-event-stream-0.4.1-h96a4043_5.conda + hash: + md5: 835b55a424b3f10e5f6258b65efda9c0 + sha256: a82aa3ac796b70f815b3e84ae0a8969fbaa6a4c6bd87ec7ca80ffc416bb0198c + category: main + optional: false +- name: aws-c-http + version: 0.8.0 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-cal: '>=0.6.9,<0.6.10.0a0' + aws-c-common: '>=0.9.12,<0.9.13.0a0' + aws-c-compression: '>=0.2.17,<0.2.18.0a0' + aws-c-io: '>=0.14.3,<0.14.4.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-http-0.8.0-h28e27ac_5.conda + hash: + md5: 6d3738fcb9c1f5a62a3364aef23de578 + sha256: 1af56b4784342dc2f6488adea98d50dc71bacc5c5d474acbd4f107776a9a8622 + category: main + optional: false +- name: aws-c-io + version: 0.14.3 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-cal: '>=0.6.9,<0.6.10.0a0' + aws-c-common: '>=0.9.12,<0.9.13.0a0' + libgcc-ng: '>=12' + s2n: '>=1.4.3,<1.4.4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-io-0.14.3-h5d4d345_1.conda + hash: + md5: f5adc023af2640a0ca1be5cd4e4f131c + sha256: f3e25f6f23c9647b89f35620dad88134b9900790ef25271a298a244629c2f3ae + category: main + optional: false +- name: aws-c-mqtt + version: 0.10.1 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-common: '>=0.9.12,<0.9.13.0a0' + aws-c-http: '>=0.8.0,<0.8.1.0a0' + aws-c-io: '>=0.14.3,<0.14.4.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-mqtt-0.10.1-h54b3c4e_3.conda + hash: + md5: 6a213b976295ad8bdfd4be8c8cff2e4c + sha256: 2d7a59dbb350fe47ba771d5749519cca62e3bac357977166a3dd525b2c9275ce + category: main + optional: false +- name: aws-c-s3 + version: 0.5.0 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-auth: '>=0.7.15,<0.7.16.0a0' + aws-c-cal: '>=0.6.9,<0.6.10.0a0' + aws-c-common: '>=0.9.12,<0.9.13.0a0' + aws-c-http: '>=0.8.0,<0.8.1.0a0' + aws-c-io: '>=0.14.3,<0.14.4.0a0' + aws-checksums: '>=0.1.17,<0.1.18.0a0' + libgcc-ng: '>=12' + openssl: '>=3.2.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-s3-0.5.0-hc48d4bd_2.conda + hash: + md5: 2ea39090e4f2c2d37f0e7ec7f8169015 + sha256: 22d20774d5005c7b0ba9fb61df2e5ceae42708d757a157276bcf11ee56475367 + category: main + optional: false +- name: aws-c-sdkutils + version: 0.1.14 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-common: '>=0.9.12,<0.9.13.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-c-sdkutils-0.1.14-hf7cfaa6_0.conda + hash: + md5: dae4bd384e4e457dafae97b9329aa26f + sha256: 4fe86f360a4d2be1bafb3333eee27fe9c3904abc665a53b978d5ebd2ceda0ff8 + category: main + optional: false +- name: aws-checksums + version: 0.1.17 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-common: '>=0.9.12,<0.9.13.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-checksums-0.1.17-hf7cfaa6_7.conda + hash: + md5: ce2877af415607a91a37e53a60366a50 + sha256: 7de02889a45c2f594a187d785ef7fa03df705d0018bfd53791da8abf708c78bb + category: main + optional: false +- name: aws-crt-cpp + version: 0.26.1 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-auth: '>=0.7.15,<0.7.16.0a0' + aws-c-cal: '>=0.6.9,<0.6.10.0a0' + aws-c-common: '>=0.9.12,<0.9.13.0a0' + aws-c-event-stream: '>=0.4.1,<0.4.2.0a0' + aws-c-http: '>=0.8.0,<0.8.1.0a0' + aws-c-io: '>=0.14.3,<0.14.4.0a0' + aws-c-mqtt: '>=0.10.1,<0.10.2.0a0' + aws-c-s3: '>=0.5.0,<0.5.1.0a0' + aws-c-sdkutils: '>=0.1.14,<0.1.15.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-crt-cpp-0.26.1-hd9c1a88_9.conda + hash: + md5: af49defa34c431a29631a34c4915a378 + sha256: c52786ba2d9a504c120226ff31e2258cf78b5229e6709e5a5010e955d787661f + category: main + optional: false +- name: aws-sdk-cpp + version: 1.11.242 + manager: conda + platform: linux-aarch64 + dependencies: + aws-c-common: '>=0.9.12,<0.9.13.0a0' + aws-c-event-stream: '>=0.4.1,<0.4.2.0a0' + aws-checksums: '>=0.1.17,<0.1.18.0a0' + aws-crt-cpp: '>=0.26.1,<0.26.2.0a0' + libcurl: '>=8.5.0,<9.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.2.0,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/aws-sdk-cpp-1.11.242-hf01a265_0.conda + hash: + md5: f930fb3e674034eb24eddf591449eea7 + sha256: f971c3ee9af85bb75952a0d98700ce9c5e661a327b70cdbcf633ec17ac1bbb5c + category: main + optional: false +- name: azure-core-cpp + version: 1.10.3 + manager: conda + platform: linux-aarch64 + dependencies: + libcurl: '>=8.5.0,<9.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.2.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/azure-core-cpp-1.10.3-hcd87347_1.conda + hash: + md5: 6d52760d398c383340c645b676d6d7c8 + sha256: b2e8f2de0101a26491b9ffbb01e0fa3f6cb279a7e662423cdf12d2cad7e8a20a + category: main + optional: false +- name: azure-storage-blobs-cpp + version: 12.10.0 + manager: conda + platform: linux-aarch64 + dependencies: + azure-core-cpp: '>=1.10.3,<2.0a0' + azure-storage-common-cpp: '>=12.5.0,<13.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/azure-storage-blobs-cpp-12.10.0-h2a328a1_0.conda + hash: + md5: 421b4ee5509aba7be1be6785444f6278 + sha256: 82ad9450285d0979dd91ac7876ec4cdaf3fc8850ba1fe5f141711483ba6b0d83 + category: main + optional: false +- name: azure-storage-common-cpp + version: 12.5.0 + manager: conda + platform: linux-aarch64 + dependencies: + azure-core-cpp: '>=1.10.3,<2.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libxml2: '>=2.12.1,<3.0.0a0' + openssl: '>=3.2.0,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/azure-storage-common-cpp-12.5.0-hee0c750_2.conda + hash: + md5: 99479410b3dcf4b5260a692a94f9d46b + sha256: 16d06ceabedacfad5ce5f1dbf9db298f3d0e6000596955f596a1ae5582ebe427 + category: main + optional: false +- name: blosc + version: 1.21.5 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + lz4-c: '>=1.9.3,<1.10.0a0' + snappy: '>=1.1.10,<2.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/blosc-1.21.5-h2f3a684_0.conda + hash: + md5: c1f53cf8a0e36464e084d9f167365552 + sha256: 4b7cecdece6e31651993bd2960f6a025d8e546b4778fff101b19e66107667860 + category: main + optional: false +- name: branca + version: 0.7.1 + manager: conda + platform: linux-aarch64 + dependencies: + jinja2: '>=3' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/branca-0.7.1-pyhd8ed1ab_0.conda + hash: + md5: 35fa1bfd27c4d4c3cd46501a9ca7bd78 + sha256: 4053ce4389a524e226eea020e2e507335e908a45d324b4f48d4b4407b17c88e3 + category: main + optional: false +- name: brotli + version: 1.1.0 + manager: conda + platform: linux-aarch64 + dependencies: + brotli-bin: 1.1.0 + libbrotlidec: 1.1.0 + libbrotlienc: 1.1.0 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-1.1.0-h31becfc_1.conda + hash: + md5: e41f5862ac746428407f3fd44d2ed01f + sha256: 1e1e46a4d16936d1bd1a605767b4cc36cf8fd3180ad776b5ba9e4c8ce64859bf + category: main + optional: false +- name: brotli-bin + version: 1.1.0 + manager: conda + platform: linux-aarch64 + dependencies: + libbrotlidec: 1.1.0 + libbrotlienc: 1.1.0 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-bin-1.1.0-h31becfc_1.conda + hash: + md5: 9e4a13596ab651ea8d77aae023d0ce3f + sha256: fd1e57615b995565939fdb9910534933c4c27ec0c37a911a2c923241dbf8ad3b + category: main + optional: false +- name: brotli-python + version: 1.1.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + python: '>=3.12.0rc3,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.1.0-py312h2aa54b4_1.conda + hash: + md5: 7253fd6feb797007a3d290bbcfd23a84 + sha256: 5762bb7d3aaea2637840a6c30dbd398d450aa9376b507dbe5db75e92d221ddd5 + category: main + optional: false +- name: bzip2 + version: 1.0.8 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h31becfc_5.conda + hash: + md5: a64e35f01e0b7a2a152eca87d33b9c87 + sha256: b9f170990625cb1eeefaca02e091dc009a64264b077166d8ed7aeb7a09e923b0 + category: main + optional: false +- name: c-ares + version: 1.26.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.26.0-h31becfc_0.conda + hash: + md5: f5094fec0d7d788152c7503140929bf2 + sha256: d9eebd51d89c68789957e92d73c0ca4651aef859c6caa5899dd0c8d46b936d8c + category: main + optional: false +- name: ca-certificates + version: 2024.2.2 + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2024.2.2-hcefe29a_0.conda + hash: + md5: 57c226edb90c4e973b9b7503537dd339 + sha256: 0f6b34d835e26e5fa97cca4985dc46f0aba551a3a23f07c6f13cca2542b8c642 + category: main + optional: false +- name: cairo + version: 1.18.0 + manager: conda + platform: linux-aarch64 + dependencies: + fontconfig: '>=2.14.2,<3.0a0' + fonts-conda-ecosystem: '' + freetype: '>=2.12.1,<3.0a0' + icu: '>=73.2,<74.0a0' + libgcc-ng: '>=12' + libglib: '>=2.78.0,<3.0a0' + libpng: '>=1.6.39,<1.7.0a0' + libstdcxx-ng: '>=12' + libxcb: '>=1.15,<1.16.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + pixman: '>=0.42.2,<1.0a0' + xorg-libice: '>=1.1.1,<2.0a0' + xorg-libsm: '>=1.2.4,<2.0a0' + xorg-libx11: '>=1.8.6,<2.0a0' + xorg-libxext: '>=1.3.4,<2.0a0' + xorg-libxrender: '>=0.9.11,<0.10.0a0' + zlib: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.0-ha13f110_0.conda + hash: + md5: 425111f8cc6945c5d1307357dd819b9b + sha256: 79b6323661b535d90aaec0eac0e91ccda88cc5917d9e597a03d7de183bc22f26 + category: main + optional: false +- name: certifi + version: 2024.2.2 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.2.2-pyhd8ed1ab_0.conda + hash: + md5: 0876280e409658fc6f9e75d035960333 + sha256: f1faca020f988696e6b6ee47c82524c7806380b37cfdd1def32f92c326caca54 + category: main + optional: false +- name: cfitsio + version: 4.3.1 + manager: conda + platform: linux-aarch64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + libcurl: '>=8.4.0,<9.0a0' + libgcc-ng: '>=12' + libgfortran-ng: '' + libgfortran5: '>=12.3.0' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/cfitsio-4.3.1-hf28c5f1_0.conda + hash: + md5: 3b1ede3e444833dbd1f6ac717ae5dfb3 + sha256: 2a68d326e05a4c68df1741ec95b2c624f665f428ca833cf57b24aeed1798cbce + category: main + optional: false +- name: charset-normalizer + version: 3.3.2 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda + hash: + md5: 7f4a9e3fcff3f6356ae99244a014da6a + sha256: 20cae47d31fdd58d99c4d2e65fbdcefa0b0de0c84e455ba9d6356a4bdbc4b5b9 + category: main + optional: false +- name: click + version: 8.1.7 + manager: conda + platform: linux-aarch64 + dependencies: + __unix: '' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda + hash: + md5: f3ad426304898027fc619827ff428eca + sha256: f0016cbab6ac4138a429e28dbcb904a90305b34b3fe41a9b89d697c90401caec + category: main + optional: false +- name: click-plugins + version: 1.1.1 + manager: conda + platform: linux-aarch64 + dependencies: + click: '>=3.0' + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/click-plugins-1.1.1-py_0.tar.bz2 + hash: + md5: 4fd2c6b53934bd7d96d1f3fdaf99b79f + sha256: ddef6e559dde6673ee504b0e29dd814d36e22b6b9b1f519fa856ee268905bf92 + category: main + optional: false +- name: cligj + version: 0.7.2 + manager: conda + platform: linux-aarch64 + dependencies: + click: '>=4.0' + python: <4.0 + url: https://conda.anaconda.org/conda-forge/noarch/cligj-0.7.2-pyhd8ed1ab_1.tar.bz2 + hash: + md5: a29b7c141d6b2de4bb67788a5f107734 + sha256: 97bd58f0cfcff56a0bcda101e26f7d936625599325beba3e3a1fa512dd7fc174 + category: main + optional: false +- name: contourpy + version: 1.2.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + numpy: '>=1.20,<2' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/contourpy-1.2.0-py312h8f0b210_0.conda + hash: + md5: 4a5ce41c9540a44e79855a33787a1da6 + sha256: d5b7ecbded0f889ee490f6f328e4b3006796f337804652e0782c7e849fb50012 + category: main + optional: false +- name: cycler + version: 0.12.1 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda + hash: + md5: 5cd86562580f274031ede6aa6aa24441 + sha256: f221233f21b1d06971792d491445fd548224641af9443739b4b7b6d5d72954a8 + category: main + optional: false +- name: expat + version: 2.5.0 + manager: conda + platform: linux-aarch64 + dependencies: + libexpat: 2.5.0 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/expat-2.5.0-hd600fc2_1.conda + hash: + md5: 6dfca4be3e0080934b1105d009747e98 + sha256: a00bae815836f8fc73e47701c25998be81284dcefab28e002efde68e0bb7eee0 + category: main + optional: false +- name: fiona + version: 1.9.5 + manager: conda + platform: linux-aarch64 + dependencies: + attrs: '>=19.2.0' + click: '>=8.0,<9.dev0' + click-plugins: '>=1.0' + cligj: '>=0.5' + gdal: '' + libgcc-ng: '>=12' + libgdal: '>=3.8.2,<3.9.0a0' + libstdcxx-ng: '>=12' + numpy: '>=1.26.2,<2.0a0' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + setuptools: '' + shapely: '' + six: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/fiona-1.9.5-py312h78df3c0_3.conda + hash: + md5: 41fd7111938baea47ecfb1dbd7be7cf6 + sha256: 2eacd5607ab903f2858313560d37726dcbec4318c154ec95ad194ad519b7cc03 + category: main + optional: false +- name: folium + version: 0.15.1 + manager: conda + platform: linux-aarch64 + dependencies: + branca: '>=0.7.0' + jinja2: '>=2.9' + numpy: '' + python: '>=3.7' + requests: '' + xyzservices: '' + url: https://conda.anaconda.org/conda-forge/noarch/folium-0.15.1-pyhd8ed1ab_0.conda + hash: + md5: 4fdfc338f6fb875b1078a7e20dbc99e2 + sha256: c0730ddfaf35e39ac92b7fd07315843e7948b2ce3361d164ec1b722dd0440c2b + category: main + optional: false +- name: font-ttf-dejavu-sans-mono + version: '2.37' + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + hash: + md5: 0c96522c6bdaed4b1566d11387caaf45 + sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b + category: main + optional: false +- name: font-ttf-inconsolata + version: '3.000' + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + hash: + md5: 34893075a5c9e55cdafac56607368fc6 + sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c + category: main + optional: false +- name: font-ttf-source-code-pro + version: '2.038' + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + hash: + md5: 4d59c254e01d9cde7957100457e2d5fb + sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 + category: main + optional: false +- name: font-ttf-ubuntu + version: '0.83' + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_1.conda + hash: + md5: 6185f640c43843e5ad6fd1c5372c3f80 + sha256: 056c85b482d58faab5fd4670b6c1f5df0986314cca3bc831d458b22e4ef2c792 + category: main + optional: false +- name: fontconfig + version: 2.14.2 + manager: conda + platform: linux-aarch64 + dependencies: + expat: '>=2.5.0,<3.0a0' + freetype: '>=2.12.1,<3.0a0' + libgcc-ng: '>=12' + libuuid: '>=2.32.1,<3.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.14.2-ha9a116f_0.conda + hash: + md5: 6d2d19ea85f9d41534cd28fdefd59a25 + sha256: 71143b04d9beeb76264a54cb42a2953ff858a95f7383531fcb3a33ac6433e7f6 + category: main + optional: false +- name: fonts-conda-ecosystem + version: '1' + manager: conda + platform: linux-aarch64 + dependencies: + fonts-conda-forge: '' + url: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + hash: + md5: fee5683a3f04bd15cbd8318b096a27ab + sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 + category: main + optional: false +- name: fonts-conda-forge + version: '1' + manager: conda + platform: linux-aarch64 + dependencies: + font-ttf-dejavu-sans-mono: '' + font-ttf-inconsolata: '' + font-ttf-source-code-pro: '' + font-ttf-ubuntu: '' + url: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + hash: + md5: f766549260d6815b0c52253f1fb1bb29 + sha256: 53f23a3319466053818540bcdf2091f253cbdbab1e0e9ae7b9e509dcaa2a5e38 + category: main + optional: false +- name: fonttools + version: 4.48.1 + manager: conda + platform: linux-aarch64 + dependencies: + brotli: '' + libgcc-ng: '>=12' + munkres: '' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/fonttools-4.48.1-py312hdd3e373_0.conda + hash: + md5: cb96e2a14e0296f2704dd55ad3627855 + sha256: 7377ccce51b96205a225016eeee579a1c182b8953404a12ed88f44f910180aa5 + category: main + optional: false +- name: freetype + version: 2.12.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libpng: '>=1.6.39,<1.7.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.12.1-hf0a5ef3_2.conda + hash: + md5: a5ab74c5bd158c3d5532b66d8d83d907 + sha256: 7af93030f4407f076dce181062360efac2cd54dce863b5d7765287a6f5382537 + category: main + optional: false +- name: freexl + version: 2.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libexpat: '>=2.5.0,<3.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + minizip: '>=4.0.1,<5.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/freexl-2.0.0-h5428426_0.conda + hash: + md5: 1338ecf4f6072e376e87f3ae6bc34170 + sha256: d1c1b82336de80f6b2045654ec980419520e32db9d54e75a41feb6180ab26c8a + category: main + optional: false +- name: gdal + version: 3.8.3 + manager: conda + platform: linux-aarch64 + dependencies: + hdf5: '>=1.14.3,<1.14.4.0a0' + libgcc-ng: '>=12' + libgdal: 3.8.3 + libstdcxx-ng: '>=12' + libxml2: '>=2.12.5,<3.0a0' + numpy: '>=1.26.3,<2.0a0' + openssl: '>=3.2.1,<4.0a0' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/gdal-3.8.3-py312hd8c316d_2.conda + hash: + md5: c85b49ea2e37b3ceb27f6cd93205d62c + sha256: 0baeb8580902a967fe32590b3eaae3d290c2f6029f72fc0df875593bee91fe04 + category: main + optional: false +- name: geopandas + version: 0.14.3 + manager: conda + platform: linux-aarch64 + dependencies: + fiona: '>=1.8.21' + folium: '' + geopandas-base: 0.14.3 + mapclassify: '>=2.4.0' + matplotlib-base: '' + python: '>=3.9' + rtree: '' + xyzservices: '' + url: https://conda.anaconda.org/conda-forge/noarch/geopandas-0.14.3-pyhd8ed1ab_0.conda + hash: + md5: d8e208e375441bf1404e9693f13f3c25 + sha256: 15bdc3d85ffa9c6601f57dd5e2780dbcbe52ca5da70164fb5bb1bb4c72b92010 + category: main + optional: false +- name: geopandas-base + version: 0.14.3 + manager: conda + platform: linux-aarch64 + dependencies: + packaging: '' + pandas: '>=1.4.0' + pyproj: '>=3.3.0' + python: '>=3.9' + shapely: '>=1.8.0' + url: https://conda.anaconda.org/conda-forge/noarch/geopandas-base-0.14.3-pyha770c72_0.conda + hash: + md5: fbac4b2194c962b97324a3f5dd7d2696 + sha256: 0a8fb5a368d19fd08f7f65dfcff563322cb34e47947cabab8fc7f187d9bc9269 + category: main + optional: false +- name: geos + version: 3.12.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/geos-3.12.1-h2f0025b_0.conda + hash: + md5: ac30e662102643639f9421aa80723e2b + sha256: 7e041dcaa524aeb7564f1cd3c7ba25ba1f1ed57c18b0516da92eccbd44844f24 + category: main + optional: false +- name: geotiff + version: 1.7.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libstdcxx-ng: '>=12' + libtiff: '>=4.6.0,<4.7.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + proj: '>=9.3.1,<9.3.2.0a0' + zlib: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/geotiff-1.7.1-h3e58e51_15.conda + hash: + md5: 11964a0f5b8b311ee1f5944f0e9a6c37 + sha256: bf76331bf9acf87e5256992501b2290c2541a53d684d4cc476ab43e65e1a007a + category: main + optional: false +- name: gettext + version: 0.21.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/gettext-0.21.1-ha18d298_0.tar.bz2 + hash: + md5: b109f1a4d22966793d61fd7f75b744c3 + sha256: b1d8ee80b7577661a8cebdfd21dd1676ba73b676d106c458d4ecdbe4a6d9c2eb + category: main + optional: false +- name: gflags + version: 2.2.2 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=7.5.0' + libstdcxx-ng: '>=7.5.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/gflags-2.2.2-h54f1f3f_1004.tar.bz2 + hash: + md5: f286d3464cc8d467c92e4f17990c98c1 + sha256: c72f18b94048df5525d8ae73a9efb8d830048b70328d63738d91d3ea54e55b91 + category: main + optional: false +- name: giflib + version: 5.2.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/giflib-5.2.1-hb4cce97_3.conda + hash: + md5: a1f16c57cf6c50399556a32e750e9461 + sha256: ae2d7dbf4e2fd5105cb7ccfff5f12c982ef0c12e22d59c23e10d9e8a5c853dc7 + category: main + optional: false +- name: glog + version: 0.6.0 + manager: conda + platform: linux-aarch64 + dependencies: + gflags: '>=2.2.2,<2.3.0a0' + libgcc-ng: '>=10.3.0' + libstdcxx-ng: '>=10.3.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/glog-0.6.0-h8ab10f1_0.tar.bz2 + hash: + md5: 9dc55595db8d7947bb253f63bbcec8ca + sha256: e41461399e2a5d139935caaa95f8264f700b058ea077708f8b0417a79ced215c + category: main + optional: false +- name: hdf4 + version: 4.2.15 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/hdf4-4.2.15-hb6ba311_7.conda + hash: + md5: e1b6676b77b9690d07ea25de48aed97e + sha256: 70d1e2d3e0b9ae1b149a31a4270adfbb5a4ceb2f8c36d17feffcd7bcb6208022 + category: main + optional: false +- name: hdf5 + version: 1.14.3 + manager: conda + platform: linux-aarch64 + dependencies: + libaec: '>=1.1.2,<2.0a0' + libcurl: '>=8.4.0,<9.0a0' + libgcc-ng: '>=12' + libgfortran-ng: '' + libgfortran5: '>=12.3.0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.2.0,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/hdf5-1.14.3-nompi_ha486f32_100.conda + hash: + md5: 87cd6b1683c0eea2ba2856700aeee2cf + sha256: 6ab7ee800d06f7dcc1fa550c44416a981e213653138eb6da325693e1bd08d918 + category: main + optional: false +- name: icu + version: '73.2' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-73.2-h787c7f5_0.conda + hash: + md5: 9d3c29d71f28452a2e843aff8cbe09d2 + sha256: aedb9c911ede5596c87e1abd763ed940fab680d71fdb953bce8e4094119d47b3 + category: main + optional: false +- name: idna + version: '3.6' + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda + hash: + md5: 1a76f09108576397c41c0b0c5bd84134 + sha256: 6ee4c986d69ce61e60a20b2459b6f2027baeba153f0a64995fd3cb47c2cc7e07 + category: main + optional: false +- name: jinja2 + version: 3.1.3 + manager: conda + platform: linux-aarch64 + dependencies: + markupsafe: '>=2.0' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda + hash: + md5: e7d8df6509ba635247ff9aea31134262 + sha256: fd517b7dd3a61eca34f8a6f9f92f306397149cae1204fce72ac3d227107dafdc + category: main + optional: false +- name: joblib + version: 1.3.2 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.7' + setuptools: '' + url: https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda + hash: + md5: 4da50d410f553db77e62ab62ffaa1abc + sha256: 31e05d47970d956206188480b038829d24ac11fe8216409d8584d93d40233878 + category: main + optional: false +- name: json-c + version: '0.17' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/json-c-0.17-h9d1147b_0.conda + hash: + md5: d29aae0f70e8082d021842256c86b49f + sha256: 4cd7c08275a91df826d0ffa2d6aa1e3279afdfaf15adcf687c6fb8dd92170c54 + category: main + optional: false +- name: kealib + version: 1.5.3 + manager: conda + platform: linux-aarch64 + dependencies: + hdf5: '>=1.14.3,<1.14.4.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/kealib-1.5.3-h4670d8b_0.conda + hash: + md5: f1949c81436aab570e9ce976697566bf + sha256: ec7fc04e016ab828f4af334bfa98c44b46dac5147b01fc6e50a5194fcdfbd695 + category: main + optional: false +- name: keyutils + version: 1.6.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=10.3.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.1-h4e544f5_0.tar.bz2 + hash: + md5: 1f24853e59c68892452ef94ddd8afd4b + sha256: 6d4233d97a9b38acbb26e1268bcf8c10a8e79c2aed7e5a385ec3769967e3e65b + category: main + optional: false +- name: kiwisolver + version: 1.4.5 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + python: '>=3.12.0rc3,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/kiwisolver-1.4.5-py312h721a97f_1.conda + hash: + md5: 3f53f74e99a5fa60e390e139bfe2ef5f + sha256: e102ed6248df697a8351f6b9f0fe40daf0d171500cd6aef8abacc2bfd83dc9d9 + category: main + optional: false +- name: krb5 + version: 1.21.2 + manager: conda + platform: linux-aarch64 + dependencies: + keyutils: '>=1.6.1,<2.0a0' + libedit: '>=3.1.20191231,<4.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.1.2,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.2-hc419048_0.conda + hash: + md5: 55b51af37bf6fdcfe06f140e62e8c8db + sha256: c3f24ead49fb7d7c29fae491bec3f090f63d77a46954eadbc4463f137e2b42cd + category: main + optional: false +- name: lcms2 + version: '2.16' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libtiff: '>=4.6.0,<4.7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.16-h922389a_0.conda + hash: + md5: ffdd8267a04c515e7ce69c727b051414 + sha256: be4847b1014d3cbbc524a53bdbf66182f86125775020563e11d914c8468dd97d + category: main + optional: false +- name: ld_impl_linux-aarch64 + version: '2.40' + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.40-h2d8c526_0.conda + hash: + md5: 16246d69e945d0b1969a6099e7c5d457 + sha256: 1ba06e8645094b340b4aee23603a6abb1b0383788180e65f3de34e655c5f577c + category: main + optional: false +- name: lerc + version: 4.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-h4de3ea5_0.tar.bz2 + hash: + md5: 1a0ffc65e03ce81559dbcb0695ad1476 + sha256: 2d09ef9b7796d83364957e420b41c32d94e628c3f0520b61c332518a7b5cd586 + category: main + optional: false +- name: libabseil + version: '20230802.1' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libabseil-20230802.1-cxx17_h2f0025b_0.conda + hash: + md5: d1d7afab9c131b52ffe11aed370d06cd + sha256: 7890ac8c806e63d2acf8b6917151ad80a17c63b832bf0926723a890d9861b856 + category: main + optional: false +- name: libaec + version: 1.1.2 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libaec-1.1.2-h2f0025b_1.conda + hash: + md5: 35cdc41045e1041d7f3bf29081b3d3cb + sha256: faf1c20e0a0f823c01913ae0243a3fc5ebe822689c95896d24f80492903b497a + category: main + optional: false +- name: libarchive + version: 3.7.2 + manager: conda + platform: linux-aarch64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + libgcc-ng: '>=12' + libxml2: '>=2.12.2,<3.0.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + lz4-c: '>=1.9.3,<1.10.0a0' + lzo: '>=2.10,<3.0a0' + openssl: '>=3.2.0,<4.0a0' + xz: '>=5.2.6,<6.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libarchive-3.7.2-hd2f85e0_1.conda + hash: + md5: a0f2e7adbcdf4041d6ee273d07ca171e + sha256: 5e58975163f0a76fa5b8dac3a26607037e1a71516dd7a80fb98e74b203450510 + category: main + optional: false +- name: libarrow + version: 15.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + aws-crt-cpp: '>=0.26.1,<0.26.2.0a0' + aws-sdk-cpp: '>=1.11.242,<1.11.243.0a0' + bzip2: '>=1.0.8,<2.0a0' + glog: '>=0.6.0,<0.7.0a0' + libabseil: '>=20230802.1,<20230803.0a0' + libbrotlidec: '>=1.1.0,<1.2.0a0' + libbrotlienc: '>=1.1.0,<1.2.0a0' + libgcc-ng: '>=12' + libgoogle-cloud: '>=2.12.0,<2.13.0a0' + libre2-11: '>=2023.6.2,<2024.0a0' + libstdcxx-ng: '>=12' + libutf8proc: '>=2.8.0,<3.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + lz4-c: '>=1.9.3,<1.10.0a0' + orc: '>=1.9.2,<1.9.3.0a0' + re2: '' + snappy: '>=1.1.10,<2.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-15.0.0-h94a09b9_2_cpu.conda + hash: + md5: ffb8a09d7c25b8239e3f06e2edbfec06 + sha256: 3d8f87d1d803f57682820ffa00b3e5d85eed3b561462d1dca78913246a9618ef + category: main + optional: false +- name: libarrow-acero + version: 15.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libarrow: 15.0.0 + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-acero-15.0.0-h2f0025b_2_cpu.conda + hash: + md5: 3ef071eab02a57d922b1dfb45e31e503 + sha256: 5e2a817c7b08b716d85516fa493be2ad2fb371f112ff70ff23ccdbc3f4390c8d + category: main + optional: false +- name: libarrow-dataset + version: 15.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libarrow: 15.0.0 + libarrow-acero: 15.0.0 + libgcc-ng: '>=12' + libparquet: 15.0.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-dataset-15.0.0-h2f0025b_2_cpu.conda + hash: + md5: 2a6e6d05bad4329ac1a663a26b64782d + sha256: 8f4740a526515df591601bb851767075eb7cfd632865c4bd40373dae9170eca8 + category: main + optional: false +- name: libarrow-flight + version: 15.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libabseil: '>=20230802.1,<20230803.0a0' + libarrow: 15.0.0 + libgcc-ng: '>=12' + libgrpc: '>=1.60.0,<1.61.0a0' + libprotobuf: '>=4.25.1,<4.25.2.0a0' + libstdcxx-ng: '>=12' + ucx: '>=1.15.0,<1.16.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-flight-15.0.0-he69d72d_2_cpu.conda + hash: + md5: 72af0cdd7747e59d32f8159526d1fb0d + sha256: a6285b577db06695e3dbab9aa29b271c1a5df2ec641060d72bd0923cac8f7e17 + category: main + optional: false +- name: libarrow-flight-sql + version: 15.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libarrow: 15.0.0 + libarrow-flight: 15.0.0 + libgcc-ng: '>=12' + libprotobuf: '>=4.25.1,<4.25.2.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-flight-sql-15.0.0-h1fc705f_2_cpu.conda + hash: + md5: e749637fe8eb97f1ecc145b33fc1981b + sha256: 6317fea94c76935ed28a91dcc9977f16ff338c0547cf5442b8b09d48e7a96192 + category: main + optional: false +- name: libarrow-gandiva + version: 15.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libarrow: 15.0.0 + libgcc-ng: '>=12' + libllvm15: '>=15.0.7,<15.1.0a0' + libre2-11: '>=2023.6.2,<2024.0a0' + libstdcxx-ng: '>=12' + libutf8proc: '>=2.8.0,<3.0a0' + openssl: '>=3.2.1,<4.0a0' + re2: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-gandiva-15.0.0-h1bc7839_2_cpu.conda + hash: + md5: 0a96e3c0088ad1d4a7e84de68b59133f + sha256: d2da9c3cc942869c00b3d8b1efc439a3f96b8b01b7f33bb276c4f488d2f90fee + category: main + optional: false +- name: libarrow-substrait + version: 15.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libarrow: 15.0.0 + libarrow-acero: 15.0.0 + libarrow-dataset: 15.0.0 + libgcc-ng: '>=12' + libprotobuf: '>=4.25.1,<4.25.2.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libarrow-substrait-15.0.0-h0599332_2_cpu.conda + hash: + md5: 3b09bcb42a4012cfd9c83b6b185f16af + sha256: 1ec25ebd78d462ef39b9f8d6b53ac1a09e3aa8b4403559cb80f4bb104fe1f30b + category: main + optional: false +- name: libblas + version: 3.9.0 + manager: conda + platform: linux-aarch64 + dependencies: + libopenblas: '>=0.3.26,<1.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libblas-3.9.0-21_linuxaarch64_openblas.conda + hash: + md5: 7358230781e5d6e76e6adacf5201bcdf + sha256: 5d1dcfc2ef54ce415ffabc8e2d94d10f8a24e10096193da24b0b62dbfe35bf32 + category: main + optional: false +- name: libboost-headers + version: 1.84.0 + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libboost-headers-1.84.0-h8af1aa0_1.conda + hash: + md5: e74d7ab5b78bf73c779961cd124d19f9 + sha256: 6b69db79b65ad26758c3ea57a63e70770c49e6eaefa5f7bc3fc6aaaac8991f5c + category: main + optional: false +- name: libbrotlicommon + version: 1.1.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlicommon-1.1.0-h31becfc_1.conda + hash: + md5: 1b219fd801eddb7a94df5bd001053ad9 + sha256: 1c3d4ea61e862eb5f1968915f6f5917ea61db9921aec30b14785775c87234060 + category: main + optional: false +- name: libbrotlidec + version: 1.1.0 + manager: conda + platform: linux-aarch64 + dependencies: + libbrotlicommon: 1.1.0 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlidec-1.1.0-h31becfc_1.conda + hash: + md5: 8db7cff89510bec0b863a0a8ee6a7bce + sha256: 1d2558efbb727f9065dd94d5f906aa68252153f80e571456d3695fa102e8a352 + category: main + optional: false +- name: libbrotlienc + version: 1.1.0 + manager: conda + platform: linux-aarch64 + dependencies: + libbrotlicommon: 1.1.0 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.1.0-h31becfc_1.conda + hash: + md5: ad3d3a826b5848d99936e4466ebbaa26 + sha256: 271fd8ef9181ad19246bf8b4273c99b9608c6eedecb6b11cd925211b8f1c6217 + category: main + optional: false +- name: libcblas + version: 3.9.0 + manager: conda + platform: linux-aarch64 + dependencies: + libblas: 3.9.0 + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.9.0-21_linuxaarch64_openblas.conda + hash: + md5: 7eb9aa7a90f067f8dbfede586cdc55cd + sha256: 86224669232944141f46b41d0ba18192c7f5af9cc3133fa89694f42701fe89fd + category: main + optional: false +- name: libcrc32c + version: 1.1.2 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=9.4.0' + libstdcxx-ng: '>=9.4.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libcrc32c-1.1.2-h01db608_0.tar.bz2 + hash: + md5: 268ee639c17ada0002fb04dd21816cc2 + sha256: b8b8c57a87da86b3ea24280fd6aa8efaf92f4e684b606bf2db5d3cb06ffbe2ea + category: main + optional: false +- name: libcurl + version: 8.5.0 + manager: conda + platform: linux-aarch64 + dependencies: + krb5: '>=1.21.2,<1.22.0a0' + libgcc-ng: '>=12' + libnghttp2: '>=1.58.0,<2.0a0' + libssh2: '>=1.11.0,<2.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.2.0,<4.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.5.0-h4e8248e_0.conda + hash: + md5: fa0f5edc06ffc25a01eed005c6dc3d8c + sha256: 4f4f8b884927d0c6fad4a8f5d7afaf789fe4f6554448ac8b416231f2f3dc7490 + category: main + optional: false +- name: libdeflate + version: '1.19' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.19-h31becfc_0.conda + hash: + md5: 014e57e35f2dc95c9a12f63d4378e093 + sha256: 77f04fced83cf1da09ffb7ef16d531ac889d944dbffe8a4dc00b61e4bae076a5 + category: main + optional: false +- name: libedit + version: 3.1.20191231 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=7.5.0' + ncurses: '>=6.2,<7.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20191231-he28a2e2_2.tar.bz2 + hash: + md5: 29371161d77933a54fccf1bb66b96529 + sha256: debc31fb2f07ba2b0363f90e455873670734082822926ba4a9556431ec0bf36d + category: main + optional: false +- name: libev + version: '4.33' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda + hash: + md5: a9a13cb143bbaa477b1ebaefbe47a302 + sha256: 973af77e297f1955dd1f69c2cbdc5ab9dfc88388a5576cd152cda178af0fd006 + category: main + optional: false +- name: libevent + version: 2.1.12 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + openssl: '>=3.1.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libevent-2.1.12-h4ba1bb4_1.conda + hash: + md5: 96ae6083cd1ac9f6bc81631ac835b317 + sha256: 01333cc7d6e6985dd5700b43660d90e9e58049182017fd24862088ecbe1458e4 + category: main + optional: false +- name: libexpat + version: 2.5.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.5.0-hd600fc2_1.conda + hash: + md5: 6cd3d0a28437b3845c260f9d71d434d7 + sha256: b4651d196d5adb0637c678d874160a318078d963caec264bda7ac07ff6a1cbc7 + category: main + optional: false +- name: libffi + version: 3.4.2 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=9.4.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.2-h3557bc0_5.tar.bz2 + hash: + md5: dddd85f4d52121fab0a8b099c5e06501 + sha256: 7e9258a102480757fe3faeb225a3ca04dffd10fecd2a958c65cdb4cdf75f2c3c + category: main + optional: false +- name: libgcc-ng + version: 13.2.0 + manager: conda + platform: linux-aarch64 + dependencies: + _openmp_mutex: '>=4.5' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-13.2.0-hf8544c7_5.conda + hash: + md5: dee934e640275d9e74e7bbd455f25162 + sha256: 869e44e1cf329198f5bea56c146207ed639b24b6281187159435b9499ecb3959 + category: main + optional: false +- name: libgdal + version: 3.8.3 + manager: conda + platform: linux-aarch64 + dependencies: + blosc: '>=1.21.5,<2.0a0' + cfitsio: '>=4.3.1,<4.3.2.0a0' + freexl: '>=2.0.0,<3.0a0' + geos: '>=3.12.1,<3.12.2.0a0' + geotiff: '>=1.7.1,<1.8.0a0' + giflib: '>=5.2.1,<5.3.0a0' + hdf4: '>=4.2.15,<4.2.16.0a0' + hdf5: '>=1.14.3,<1.14.4.0a0' + json-c: '>=0.17,<0.18.0a0' + kealib: '>=1.5.3,<1.6.0a0' + lerc: '>=4.0.0,<5.0a0' + libaec: '>=1.1.2,<2.0a0' + libarchive: '>=3.7.2,<3.8.0a0' + libcurl: '>=8.5.0,<9.0a0' + libdeflate: '>=1.19,<1.20.0a0' + libexpat: '>=2.5.0,<3.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libkml: '>=1.3.0,<1.4.0a0' + libnetcdf: '>=4.9.2,<4.9.3.0a0' + libpng: '>=1.6.42,<1.7.0a0' + libpq: '>=16.1,<17.0a0' + libspatialite: '>=5.1.0,<5.2.0a0' + libsqlite: '>=3.44.2,<4.0a0' + libstdcxx-ng: '>=12' + libtiff: '>=4.6.0,<4.7.0a0' + libuuid: '>=2.38.1,<3.0a0' + libwebp-base: '>=1.3.2,<2.0a0' + libxml2: '>=2.12.5,<3.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + lz4-c: '>=1.9.3,<1.10.0a0' + openjpeg: '>=2.5.0,<3.0a0' + openssl: '>=3.2.1,<4.0a0' + pcre2: '>=10.42,<10.43.0a0' + poppler: '>=24.2.0,<24.3.0a0' + postgresql: '' + proj: '>=9.3.1,<9.3.2.0a0' + tiledb: '>=2.19.1,<2.20.0a0' + xerces-c: '>=3.2.5,<3.3.0a0' + xz: '>=5.2.6,<6.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libgdal-3.8.3-h4c8926b_2.conda + hash: + md5: b5b3807eb7445030c97075d7026490ea + sha256: 99e69e2925d1bdb5bd56585fa69c9458163823a0adfb73d2f7906891edef7665 + category: main + optional: false +- name: libgfortran-ng + version: 13.2.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgfortran5: 13.2.0 + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-ng-13.2.0-he9431aa_5.conda + hash: + md5: fab7c6a8c84492e18cbe578820e97a56 + sha256: a7e5d1ac34118a4fad8286050af0146226d2fb2bd63e7a1066dc4dae7ba42daa + category: main + optional: false +- name: libgfortran5 + version: 13.2.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=13.2.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-13.2.0-h582850c_5.conda + hash: + md5: 547486aac825d236de3beecb927b389c + sha256: f778346e85eb19bca36d1a5c8cddf8e089dcd6799b8f3e1b3f2d5a3157920827 + category: main + optional: false +- name: libglib + version: 2.78.3 + manager: conda + platform: linux-aarch64 + dependencies: + gettext: '>=0.21.1,<1.0a0' + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + pcre2: '>=10.42,<10.43.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.78.3-h311d5f7_0.conda + hash: + md5: 09e44253dee99895f02cfad44dd8fea4 + sha256: 4e400e259c42abf2850c660d2f484630f60d71be1aa66f4c6e43effed68d6961 + category: main + optional: false +- name: libgomp + version: 13.2.0 + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-13.2.0-hf8544c7_5.conda + hash: + md5: 379be2f115ffb73860e4e260dd2170b7 + sha256: a98d4f242a351feb7983a28e7d6a0ca51da764c6233ea3dfc776975a3aba8a01 + category: main + optional: false +- name: libgoogle-cloud + version: 2.12.0 + manager: conda + platform: linux-aarch64 + dependencies: + libabseil: '>=20230802.1,<20230803.0a0' + libcrc32c: '>=1.1.2,<1.2.0a0' + libcurl: '>=8.5.0,<9.0a0' + libgcc-ng: '>=12' + libgrpc: '>=1.60.0,<1.61.0a0' + libprotobuf: '>=4.25.1,<4.25.2.0a0' + libstdcxx-ng: '>=12' + openssl: '>=3.2.0,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libgoogle-cloud-2.12.0-h3b99733_5.conda + hash: + md5: 78da954aaa5fb664f2035215d5091a5b + sha256: beedcca10ad7825903e03c78fcb24bac120c1a2d1e737e77901cbd2b44173230 + category: main + optional: false +- name: libgrpc + version: 1.60.1 + manager: conda + platform: linux-aarch64 + dependencies: + c-ares: '>=1.26.0,<2.0a0' + libabseil: '>=20230802.1,<20230803.0a0' + libgcc-ng: '>=12' + libprotobuf: '>=4.25.1,<4.25.2.0a0' + libre2-11: '>=2023.6.2,<2024.0a0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.2.1,<4.0a0' + re2: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libgrpc-1.60.1-heeb7df3_0.conda + hash: + md5: e3c79b6da73add66a7b61b351b047c83 + sha256: 7ddbdda8e5de3c5dabc6bfbc6c5477c3a498ad1595880fcaaac2bdba9e89bec9 + category: main + optional: false +- name: libiconv + version: '1.17' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.17-h31becfc_2.conda + hash: + md5: 9a8eb13f14de7d761555a98712e6df65 + sha256: a30e09d089cb75a0d5b8e5c354694c1317da98261185ed65aa3793e741060614 + category: main + optional: false +- name: libjpeg-turbo + version: 3.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.0.0-h31becfc_1.conda + hash: + md5: ed24e702928be089d9ba3f05618515c6 + sha256: 675bc1f2a8581cd34a86c412663ec29c5f90c1d9f8d11866aa1ade5cdbdf8429 + category: main + optional: false +- name: libkml + version: 1.3.0 + manager: conda + platform: linux-aarch64 + dependencies: + libboost-headers: '' + libexpat: '>=2.5.0,<3.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + uriparser: '>=0.9.7,<1.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libkml-1.3.0-h7d16752_1018.conda + hash: + md5: 0a2cb881ed5cf04e6e05079ee0a7a18b + sha256: 3695e5046f617307a9c2b01763d81fc584c900d2da1b7186fe54d40c16cacc4c + category: main + optional: false +- name: liblapack + version: 3.9.0 + manager: conda + platform: linux-aarch64 + dependencies: + libblas: 3.9.0 + url: https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.9.0-21_linuxaarch64_openblas.conda + hash: + md5: ab08b651e3630c20d3032e59859f34f7 + sha256: 87c110c6a1171c62d6a8802098c4186dcc8eca0ee2d0376a843e0cd025096e4a + category: main + optional: false +- name: libllvm15 + version: 15.0.7 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libxml2: '>=2.12.1,<3.0.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libllvm15-15.0.7-hb4f23b0_4.conda + hash: + md5: 8d7aa8eae04dc19426a417528d7041eb + sha256: 12da3344f2ef37dcb80b4e2d106cf36ebc267c3be6211a8306dd1dbf07399d61 + category: main + optional: false +- name: libnetcdf + version: 4.9.2 + manager: conda + platform: linux-aarch64 + dependencies: + blosc: '>=1.21.5,<2.0a0' + bzip2: '>=1.0.8,<2.0a0' + hdf4: '>=4.2.15,<4.2.16.0a0' + hdf5: '>=1.14.3,<1.14.4.0a0' + libaec: '>=1.1.2,<2.0a0' + libcurl: '>=8.5.0,<9.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libxml2: '>=2.12.2,<3.0.0a0' + libzip: '>=1.10.1,<2.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.2.0,<4.0a0' + zlib: '' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libnetcdf-4.9.2-nompi_h33102a8_113.conda + hash: + md5: ea31ab8d4f2168dd9e9f5d15ede2bb48 + sha256: 008812a7ea975706fcd451576c67f48c380cb03260da8caed51fcb8570bdd0a5 + category: main + optional: false +- name: libnghttp2 + version: 1.58.0 + manager: conda + platform: linux-aarch64 + dependencies: + c-ares: '>=1.23.0,<2.0a0' + libev: '>=4.33,<5.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.2.0,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.58.0-hb0e430d_1.conda + hash: + md5: 8f724cdddffa79152de61f5564a3526b + sha256: ecc11e4f92f9d5830a90d42b4db55c66c4ad531e00dcf30d55171d934a568cb5 + category: main + optional: false +- name: libnsl + version: 2.0.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda + hash: + md5: c14f32510f694e3185704d89967ec422 + sha256: fd18c2b75d7411096428d36a70b36b1a17e31f7b8956b6905d145792d49e97f8 + category: main + optional: false +- name: libnuma + version: 2.0.16 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libnuma-2.0.16-hb4cce97_1.conda + hash: + md5: a63d3c8b8384e64056a8c4bfd80edbdd + sha256: 2f26e79ffadcf679033ef13b9dc72292c65e0511aa045059c2a4f4c8c2c74a32 + category: main + optional: false +- name: libopenblas + version: 0.3.26 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libgfortran-ng: '' + libgfortran5: '>=12.3.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.26-pthreads_h5a5ec62_0.conda + hash: + md5: 2ea496754b596063335b3aeaa2b982ac + sha256: b72719014a86f69162398fc32af0f23e6e1746ec795f7c5d38ad5998a78eb6f8 + category: main + optional: false +- name: libparquet + version: 15.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libarrow: 15.0.0 + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libthrift: '>=0.19.0,<0.19.1.0a0' + openssl: '>=3.2.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libparquet-15.0.0-hb18b541_2_cpu.conda + hash: + md5: 9813a3ebd65bb708b1803b055bd5f556 + sha256: 93f57ca698339dd130ef439f81bdb8578f42ab5ca00acbf1e4550be882e2793c + category: main + optional: false +- name: libpng + version: 1.6.42 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.42-h194ca79_0.conda + hash: + md5: b8ff00cc9a5184726baea61244f8bec3 + sha256: 035ab9c6a38dedd8ffc28f7c1ca0bed4196f7f8631e9ac74695a3630d2bb527b + category: main + optional: false +- name: libpq + version: '16.2' + manager: conda + platform: linux-aarch64 + dependencies: + krb5: '>=1.21.2,<1.22.0a0' + libgcc-ng: '>=12' + openssl: '>=3.2.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libpq-16.2-h58720eb_0.conda + hash: + md5: ca0572c11209701741812f657ebeef82 + sha256: 439633fee193ad250d97da135a7fb1c497b4f496ec6baabcb73bdba554926943 + category: main + optional: false +- name: libprotobuf + version: 4.25.1 + manager: conda + platform: linux-aarch64 + dependencies: + libabseil: '>=20230802.1,<20230803.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libprotobuf-4.25.1-h87e877f_1.conda + hash: + md5: c129f45da1472e472c28304046a92d9d + sha256: 826bee292ebf91b1cb3e809683d9b6bdbc2398d0a41a1b4d3e64d98f3db14d75 + category: main + optional: false +- name: libre2-11 + version: 2023.06.02 + manager: conda + platform: linux-aarch64 + dependencies: + libabseil: '>=20230802.1,<20230803.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libre2-11-2023.06.02-hf48c5ca_0.conda + hash: + md5: 364a9630c8e1d565547c887e051e4c08 + sha256: f2b1fecd4ce4f43ff3cab6ed8cb6efdaf9f69f138a7f78d97910cbd9672e19f7 + category: main + optional: false +- name: librttopo + version: 1.1.0 + manager: conda + platform: linux-aarch64 + dependencies: + geos: '>=3.12.1,<3.12.2.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/librttopo-1.1.0-hd8968fb_15.conda + hash: + md5: 5df2305d559d0e956da65304bbaa9ba4 + sha256: d73cb2055f83ada5a3c9c52009f6341ff95c4a0f2581029b2b6dbf03a381ad78 + category: main + optional: false +- name: libspatialindex + version: 1.9.3 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=9.3.0' + libstdcxx-ng: '>=9.3.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libspatialindex-1.9.3-h01db608_4.tar.bz2 + hash: + md5: b68ef401a75f73e542f30851dc8eed49 + sha256: b9ec8cedfb185cb651cc422ef9d6c65b6a4f5ec3e179fdc00046feefde9a8432 + category: main + optional: false +- name: libspatialite + version: 5.1.0 + manager: conda + platform: linux-aarch64 + dependencies: + freexl: '>=2.0.0,<3.0a0' + geos: '>=3.12.1,<3.12.2.0a0' + libgcc-ng: '>=12' + librttopo: '>=1.1.0,<1.2.0a0' + libsqlite: '>=3.44.2,<4.0a0' + libstdcxx-ng: '>=12' + libxml2: '>=2.12.2,<3.0.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + proj: '>=9.3.1,<9.3.2.0a0' + sqlite: '' + zlib: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libspatialite-5.1.0-h896d346_4.conda + hash: + md5: 7333624f70da943d90ef933c22c04c76 + sha256: d032fb3c3d1141d735e17e22b8c691de919891ebe32b3dd4f1493c1bc048ecf5 + category: main + optional: false +- name: libsqlite + version: 3.45.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.45.1-h194ca79_0.conda + hash: + md5: 4190198deb1ed253eb938f6a6d92ff4f + sha256: 2aeb33230d074c9c91605def8296475dc0c064c23bd50ccfacb8336ec4bfc422 + category: main + optional: false +- name: libssh2 + version: 1.11.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.1.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.0-h492db2e_0.conda + hash: + md5: 45532845e121677ad328c9af9953f161 + sha256: 409163dd4a888b9266369f1bce57b5ca56c216e34249637c3e10eb404e356171 + category: main + optional: false +- name: libstdcxx-ng + version: 13.2.0 + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-13.2.0-h9a76618_5.conda + hash: + md5: 1b79d37dce0fad96bdf3de03925f43b4 + sha256: c209f23a8a497fc87107a68b6bbc8d2089cf15fd4015b558dfdce63544379b05 + category: main + optional: false +- name: libthrift + version: 0.19.0 + manager: conda + platform: linux-aarch64 + dependencies: + libevent: '>=2.1.12,<2.1.13.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.1.3,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libthrift-0.19.0-h043aeee_1.conda + hash: + md5: 591ef1567ed4989d824fe35b45e3ae68 + sha256: 83d38df283ae258eb73807442ccee62364cf50b853d238a5b03092374c7bcf45 + category: main + optional: false +- name: libtiff + version: 4.6.0 + manager: conda + platform: linux-aarch64 + dependencies: + lerc: '>=4.0.0,<5.0a0' + libdeflate: '>=1.19,<1.20.0a0' + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libstdcxx-ng: '>=12' + libwebp-base: '>=1.3.2,<2.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + xz: '>=5.2.6,<6.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.6.0-h1708d11_2.conda + hash: + md5: d5638e110e7f22e2602a8edd20656720 + sha256: e6aecca5bbf354ab34fb04d8d6ef4a50477f64997c368d734cc5d1d8b1a21d3a + category: main + optional: false +- name: libutf8proc + version: 2.8.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libutf8proc-2.8.0-h4e544f5_0.tar.bz2 + hash: + md5: bf0defbd8ac06270fb5ec05c85fb3c96 + sha256: c1956b64ad9613c66cf87398f5e2c36d071034a93892da7e8cc22e75cface878 + category: main + optional: false +- name: libuuid + version: 2.38.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda + hash: + md5: 000e30b09db0b7c775b21695dff30969 + sha256: 616277b0c5f7616c2cdf36f6c316ea3f9aa5bb35f2d4476a349ab58b9b91675f + category: main + optional: false +- name: libwebp-base + version: 1.3.2 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.3.2-h31becfc_0.conda + hash: + md5: 1490de434d2a2c06a98af27641a2ffff + sha256: a85484d8399bfa310512fe94863c8da3b224ac13f5e97736da65be6f509a8bf8 + category: main + optional: false +- name: libxcb + version: '1.15' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + pthread-stubs: '' + xorg-libxau: '' + xorg-libxdmcp: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.15-h2a766a3_0.conda + hash: + md5: eb3d8c8170e3d03f2564ed2024aa00c8 + sha256: d159fcdb8b74187b0bd32f2d9b3a9191bc8b786a97e413aa66e19c39ba7050a0 + category: main + optional: false +- name: libxcrypt + version: 4.4.36 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda + hash: + md5: b4df5d7d4b63579d081fd3a4cf99740e + sha256: 6b46c397644091b8a26a3048636d10b989b1bf266d4be5e9474bf763f828f41f + category: main + optional: false +- name: libxml2 + version: 2.12.5 + manager: conda + platform: linux-aarch64 + dependencies: + icu: '>=73.2,<74.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + xz: '>=5.2.6,<6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.12.5-h3091e33_0.conda + hash: + md5: 2fcb5d64474a337f2a4213ec1dd40ce2 + sha256: 34f7a007fa23b70c56358738d7ba801df9a923422f2dcd23f3b2ca790ea2460a + category: main + optional: false +- name: libzip + version: 1.10.1 + manager: conda + platform: linux-aarch64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.1.2,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libzip-1.10.1-h4156a30_3.conda + hash: + md5: ad9400456170b46f2615bdd48dff87fe + sha256: 4b1a653eeb5a139431fb074830b7a099d111594b1867363772f27ac84dee0acd + category: main + optional: false +- name: libzlib + version: 1.2.13 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.2.13-h31becfc_5.conda + hash: + md5: b213aa87eea9491ef7b129179322e955 + sha256: aeeefbb61e5e8227e53566d5e42dbb49e120eb99109996bf0dbfde8f180747a7 + category: main + optional: false +- name: lz4-c + version: 1.9.4 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.9.4-hd600fc2_0.conda + hash: + md5: 500145a83ed07ce79c8cef24252f366b + sha256: 076870eb72411f41c46598c7582a2f3f42ba94c526a2d60a0c8f70a0a7a64429 + category: main + optional: false +- name: lzo + version: '2.10' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=7.5.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/lzo-2.10-h516909a_1000.tar.bz2 + hash: + md5: ef5661339990c399c68c71cfb341e6d7 + sha256: d322607e1b113a3fdaf949872e6fb3c43bc61bb8b698bd756e730c5f337e4c94 + category: main + optional: false +- name: mapclassify + version: 2.6.1 + manager: conda + platform: linux-aarch64 + dependencies: + networkx: '>=2.7' + numpy: '>=1.23' + pandas: '>=1.4,!=1.5.0' + python: '>=3.9' + scikit-learn: '>=1.0' + scipy: '>=1.8' + url: https://conda.anaconda.org/conda-forge/noarch/mapclassify-2.6.1-pyhd8ed1ab_0.conda + hash: + md5: 6aceae1ad4f16cf7b73ee04189947f98 + sha256: 204ab8b242229d422b33cfec07ea61cefa8bd22375a16658afbabaafce031d64 + category: main + optional: false +- name: markupsafe + version: 2.1.5 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-2.1.5-py312h9ef2f89_0.conda + hash: + md5: a00135adde3dfe19b9962c3c767c2129 + sha256: dbd85f5230b69165fe9d1e66ed60ae697da314ed041f485be09b8c1fbcc7af3a + category: main + optional: false +- name: matplotlib-base + version: 3.8.2 + manager: conda + platform: linux-aarch64 + dependencies: + certifi: '>=2020.06.20' + contourpy: '>=1.0.1' + cycler: '>=0.10' + fonttools: '>=4.22.0' + freetype: '>=2.12.1,<3.0a0' + kiwisolver: '>=1.3.1' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + numpy: '>=1.26.0,<2.0a0' + packaging: '>=20.0' + pillow: '>=8' + pyparsing: '>=2.3.1' + python: '>=3.12,<3.13.0a0' + python-dateutil: '>=2.7' + python_abi: 3.12.* + tk: '>=8.6.13,<8.7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-base-3.8.2-py312h132ec79_0.conda + hash: + md5: 0d505c874fc7a33b13669d5222465e02 + sha256: 6b578feebc06cd87d66d42b89da35512b17d897a07229fc48f63c09f80bee51e + category: main + optional: false +- name: minizip + version: 4.0.4 + manager: conda + platform: linux-aarch64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.2.0,<4.0a0' + xz: '>=5.2.6,<6.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/minizip-4.0.4-hb75dd74_0.conda + hash: + md5: 8bfd9232a180bae998793ebde11f8a77 + sha256: 1c8ad91cc07ddc36956918bf2c006b7cc31567a80a2d8709f600427324fea6c3 + category: main + optional: false +- name: munkres + version: 1.1.4 + manager: conda + platform: linux-aarch64 + dependencies: + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2 + hash: + md5: 2ba8498c1018c1e9c61eb99b973dfe19 + sha256: f86fb22b58e93d04b6f25e0d811b56797689d598788b59dcb47f59045b568306 + category: main + optional: false +- name: ncurses + version: '6.4' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.4-h0425590_2.conda + hash: + md5: 4ff0a396150dedad4269e16e5810f769 + sha256: d71cf2f2b1a9fae7ee2455a35a890423838e9594294beb4a002fe7c7b10bc086 + category: main + optional: false +- name: networkx + version: 3.2.1 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.9' + url: https://conda.anaconda.org/conda-forge/noarch/networkx-3.2.1-pyhd8ed1ab_0.conda + hash: + md5: 425fce3b531bed6ec3c74fab3e5f0a1c + sha256: 7629aa4f9f8cdff45ea7a4701fe58dccce5bf2faa01c26eb44cbb27b7e15ca9d + category: main + optional: false +- name: nspr + version: '4.35' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/nspr-4.35-h4de3ea5_0.conda + hash: + md5: 7a392f26f76fc55354c8ed60c2b99162 + sha256: 23ff7274a021dd87966277b271e5d0944fcc8b893f4920cb46dd4224604218cc + category: main + optional: false +- name: nss + version: '3.97' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libsqlite: '>=3.44.2,<4.0a0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + nspr: '>=4.35,<5.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/nss-3.97-hc5a5cc2_0.conda + hash: + md5: 69d61fcfcd726888d5c2add225461a35 + sha256: 0f8c2fc6e1b1c49db58a236150b638e990db8c8dcff592b08187ed46bba11995 + category: main + optional: false +- name: numpy + version: 1.26.4 + manager: conda + platform: linux-aarch64 + dependencies: + libblas: '>=3.9.0,<4.0a0' + libcblas: '>=3.9.0,<4.0a0' + libgcc-ng: '>=12' + liblapack: '>=3.9.0,<4.0a0' + libstdcxx-ng: '>=12' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/numpy-1.26.4-py312h470d778_0.conda + hash: + md5: 9cebf5a06cb87d4569cd68df887af476 + sha256: 23767677a7790bee5457d5e75ebd508b9a31c5354216f4310dd1acfca3f7a6f9 + category: main + optional: false +- name: openjpeg + version: 2.5.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libpng: '>=1.6.39,<1.7.0a0' + libstdcxx-ng: '>=12' + libtiff: '>=4.6.0,<4.7.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/openjpeg-2.5.0-h0d9d63b_3.conda + hash: + md5: 123f5df3bc7f0e23c6950fddb97d1f43 + sha256: 1e897431b207d531e881c2137a8983ebe679030f0c9188777e2c163e7e594389 + category: main + optional: false +- name: openssl + version: 3.2.1 + manager: conda + platform: linux-aarch64 + dependencies: + ca-certificates: '' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.2.1-h31becfc_0.conda + hash: + md5: b7e7c53240214ae96f52a440c0b0126a + sha256: 952ef5de4e3913621a89ca2eb8186683bb73832527219c6443c260a6b46cd149 + category: main + optional: false +- name: orc + version: 1.9.2 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libprotobuf: '>=4.25.1,<4.25.2.0a0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + lz4-c: '>=1.9.3,<1.10.0a0' + snappy: '>=1.1.10,<2.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/orc-1.9.2-h5960ff3_1.conda + hash: + md5: 33fba0519791e92eb6c5e807f82b9f63 + sha256: 128624682cff71fb2c305c1522f8575279c0ceb1f71c91e4c862fc41a57d95e1 + category: main + optional: false +- name: packaging + version: '23.2' + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda + hash: + md5: 79002079284aa895f883c6b7f3f88fd6 + sha256: 69b3ace6cca2dab9047b2c24926077d81d236bef45329d264b394001e3c3e52f + category: main + optional: false +- name: pandas + version: 2.2.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + numpy: '>=1.26.3,<2.0a0' + python: '>=3.12,<3.13.0a0' + python-dateutil: '>=2.8.1' + python-tzdata: '>=2022a' + python_abi: 3.12.* + pytz: '>=2020.1' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/pandas-2.2.0-py312hc56aa73_0.conda + hash: + md5: 6bd70fe15450544d7ab4fc2a546da7e1 + sha256: 712c74f09725a48a5609ed72413ec79954311af96f3eda9a2e757694e2749995 + category: main + optional: false +- name: pcre2 + version: '10.42' + manager: conda + platform: linux-aarch64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.42-hd0f9c67_0.conda + hash: + md5: 683162253dd3b6c4d21bf037e59455f4 + sha256: 7ef11cc37800dcc4693c6f827e3cb58bc8a8cefe92b4307c6826845b3f198364 + category: main + optional: false +- name: pillow + version: 10.2.0 + manager: conda + platform: linux-aarch64 + dependencies: + freetype: '>=2.12.1,<3.0a0' + lcms2: '>=2.16,<3.0a0' + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libtiff: '>=4.6.0,<4.7.0a0' + libwebp-base: '>=1.3.2,<2.0a0' + libxcb: '>=1.15,<1.16.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + openjpeg: '>=2.5.0,<3.0a0' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + tk: '>=8.6.13,<8.7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/pillow-10.2.0-py312h1e2a6dd_0.conda + hash: + md5: f5556f9618687237ead7b55de1bcd80c + sha256: 5455441dc061d08f4554a92c81b7d701f0ceb65a63305e5094c92aa89c5ee95f + category: main + optional: false +- name: pixman + version: 0.43.2 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.43.2-h2f0025b_0.conda + hash: + md5: 896686714eea591ad0c0891800c571fd + sha256: 5afb7399abf43abbe8430da48be1452027ff26d949038f33e0a2adde100174aa + category: main + optional: false +- name: poppler + version: 24.02.0 + manager: conda + platform: linux-aarch64 + dependencies: + cairo: '>=1.18.0,<2.0a0' + fontconfig: '>=2.14.2,<3.0a0' + fonts-conda-ecosystem: '' + freetype: '>=2.12.1,<3.0a0' + lcms2: '>=2.16,<3.0a0' + libcurl: '>=8.5.0,<9.0a0' + libgcc-ng: '>=12' + libglib: '>=2.78.3,<3.0a0' + libiconv: '>=1.17,<2.0a0' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libpng: '>=1.6.42,<1.7.0a0' + libstdcxx-ng: '>=12' + libtiff: '>=4.6.0,<4.7.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + nspr: '>=4.35,<5.0a0' + nss: '>=3.97,<4.0a0' + openjpeg: '>=2.5.0,<3.0a0' + poppler-data: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/poppler-24.02.0-h3cd87ed_0.conda + hash: + md5: 6e55400bd072c8adfb30cf3891a0eb3d + sha256: ad8ee9bc8307fc60fcc89f850237870fd21db1afe972e13bfa448771c9e0c431 + category: main + optional: false +- name: poppler-data + version: 0.4.12 + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/poppler-data-0.4.12-hd8ed1ab_0.conda + hash: + md5: d8d7293c5b37f39b2ac32940621c6592 + sha256: 2f227e17b3c0346112815faa605502b66c1c4511a856127f2899abf15a98a2cf + category: main + optional: false +- name: postgresql + version: '16.2' + manager: conda + platform: linux-aarch64 + dependencies: + krb5: '>=1.21.2,<1.22.0a0' + libgcc-ng: '>=12' + libpq: '16.2' + libxml2: '>=2.12.5,<3.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + openssl: '>=3.2.1,<4.0a0' + readline: '>=8.2,<9.0a0' + tzcode: '' + tzdata: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/postgresql-16.2-he703394_0.conda + hash: + md5: 7d74ee5fdce72a81c2f2a31bb614a561 + sha256: 93be901453e588baebb2ff5ba624e24812a7892ae86f415e2439b16b1e2642c0 + category: main + optional: false +- name: proj + version: 9.3.1 + manager: conda + platform: linux-aarch64 + dependencies: + libcurl: '>=8.4.0,<9.0a0' + libgcc-ng: '>=12' + libsqlite: '>=3.44.2,<4.0a0' + libstdcxx-ng: '>=12' + libtiff: '>=4.6.0,<4.7.0a0' + sqlite: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/proj-9.3.1-h7b42f86_0.conda + hash: + md5: fa6ab94a4d428b968daf32cd556fea81 + sha256: f70a317de2dfeec29fd4dd3f7642275cbb51b5a58d667f3e7c1ad2f3fb496d4c + category: main + optional: false +- name: pthread-stubs + version: '0.4' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=7.5.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-hb9de7d4_1001.tar.bz2 + hash: + md5: d0183ec6ce0b5aaa3486df25fa5f0ded + sha256: f1d7ff5e06cc515ec82010537813c796369f8e9dde46ce3f4fa1a9f70bc7db7d + category: main + optional: false +- name: pyarrow + version: 15.0.0 + manager: conda + platform: linux-aarch64 + dependencies: + libarrow: 15.0.0 + libarrow-acero: 15.0.0 + libarrow-dataset: 15.0.0 + libarrow-flight: 15.0.0 + libarrow-flight-sql: 15.0.0 + libarrow-gandiva: 15.0.0 + libarrow-substrait: 15.0.0 + libgcc-ng: '>=12' + libparquet: 15.0.0 + libstdcxx-ng: '>=12' + numpy: '>=1.26.3,<2.0a0' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/pyarrow-15.0.0-py312h2f65cca_2_cpu.conda + hash: + md5: 3509913255b9118b674a0d1635cf3f44 + sha256: 02853641dab97f08d87e8ac8c6dee05b106fc5822b6b8ba9bb49e4d06af425b1 + category: main + optional: false +- name: pyparsing + version: 3.1.1 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.1-pyhd8ed1ab_0.conda + hash: + md5: 176f7d56f0cfe9008bdf1bccd7de02fb + sha256: 4a1332d634b6c2501a973655d68f08c9c42c0bd509c349239127b10572b8354b + category: main + optional: false +- name: pyproj + version: 3.6.1 + manager: conda + platform: linux-aarch64 + dependencies: + certifi: '' + libgcc-ng: '>=12' + proj: '>=9.3.1,<9.3.2.0a0' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/pyproj-3.6.1-py312hb621bc5_5.conda + hash: + md5: e43cd567d337c51b83ecc46364971a90 + sha256: b8e1a68b9ed458926db6259518c4467395a16783881e3a4ae316bb1de559f49e + category: main + optional: false +- name: pysocks + version: 1.7.1 + manager: conda + platform: linux-aarch64 + dependencies: + __unix: '' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 + hash: + md5: 2a7de29fb590ca14b5243c4c812c8025 + sha256: a42f826e958a8d22e65b3394f437af7332610e43ee313393d1cf143f0a2d274b + category: main + optional: false +- name: python + version: 3.12.1 + manager: conda + platform: linux-aarch64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + ld_impl_linux-aarch64: '>=2.36.1' + libexpat: '>=2.5.0,<3.0a0' + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + libnsl: '>=2.0.1,<2.1.0a0' + libsqlite: '>=3.44.2,<4.0a0' + libuuid: '>=2.38.1,<3.0a0' + libxcrypt: '>=4.4.36' + libzlib: '>=1.2.13,<1.3.0a0' + ncurses: '>=6.4,<7.0a0' + openssl: '>=3.2.0,<4.0a0' + readline: '>=8.2,<9.0a0' + tk: '>=8.6.13,<8.7.0a0' + tzdata: '' + xz: '>=5.2.6,<6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.12.1-h43d1f9e_1_cpython.conda + hash: + md5: 886aaa760e922b4f7e2522e2e0abd778 + sha256: 752df0bb0c442e5f4d4464662dbc7bc2d1cacd0d08f93d6fcc49be053f276f80 + category: main + optional: false +- name: python-dateutil + version: 2.8.2 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.6' + six: '>=1.5' + url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2 + hash: + md5: dd999d1cc9f79e67dbb855c8924c7984 + sha256: 54d7785c7678166aa45adeaccfc1d2b8c3c799ca2dc05d4a82bb39b1968bd7da + category: main + optional: false +- name: python-tzdata + version: '2024.1' + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda + hash: + md5: 98206ea9954216ee7540f0c773f2104d + sha256: 9da9a849d53705dee450b83507df1ca8ffea5f83bd21a215202221f1c492f8ad + category: main + optional: false +- name: python_abi + version: '3.12' + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-aarch64/python_abi-3.12-4_cp312.conda + hash: + md5: 6c09f8e580146d88f649780cebed01de + sha256: 4f4c3389b722cac9bf39183221332ab69e468351030ec5359042b50c5d975a15 + category: main + optional: false +- name: pytz + version: '2024.1' + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda + hash: + md5: 3eeeeb9e4827ace8c0c1419c85d590ad + sha256: 1a7d6b233f7e6e3bbcbad054c8fd51e690a67b129a899a056a5e45dd9f00cb41 + category: main + optional: false +- name: re2 + version: 2023.06.02 + manager: conda + platform: linux-aarch64 + dependencies: + libre2-11: 2023.06.02 + url: https://conda.anaconda.org/conda-forge/linux-aarch64/re2-2023.06.02-h887e66c_0.conda + hash: + md5: 25adcadc54ca4932c6230f8da94d7c37 + sha256: 4aaea72a68e104e703a67cc7cd9006ca69b201db0ed224fed885ca6987406544 + category: main + optional: false +- name: readline + version: '8.2' + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + ncurses: '>=6.3,<7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8fc344f_1.conda + hash: + md5: 105eb1e16bf83bfb2eb380a48032b655 + sha256: 4c99f7417419734e3797d45bc355e61c26520e111893b0d7087a01a7fbfbe3dd + category: main + optional: false +- name: requests + version: 2.31.0 + manager: conda + platform: linux-aarch64 + dependencies: + certifi: '>=2017.4.17' + charset-normalizer: '>=2,<4' + idna: '>=2.5,<4' + python: '>=3.7' + urllib3: '>=1.21.1,<3' + url: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda + hash: + md5: a30144e4156cdbb236f99ebb49828f8b + sha256: 9f629d6fd3c8ac5f2a198639fe7af87c4db2ac9235279164bfe0fcb49d8c4bad + category: main + optional: false +- name: rtree + version: 1.2.0 + manager: conda + platform: linux-aarch64 + dependencies: + libspatialindex: '>=1.9.3,<1.9.4.0a0' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/rtree-1.2.0-py312h3b32434_0.conda + hash: + md5: e3510a703f11a062a3b3640b8ac3b8d8 + sha256: e78e032543e2138cd18905bf692d803888b2b089669f6d722f4bc95e2472a0b8 + category: main + optional: false +- name: s2n + version: 1.4.3 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + openssl: '>=3.2.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/s2n-1.4.3-h5a25046_0.conda + hash: + md5: e5ef3389587af1374d830323ffdc007a + sha256: 6d6012c4047484d806c2a7c42c702361d4fbc3640708013a36d6b1c61f81542e + category: main + optional: false +- name: scikit-learn + version: 1.4.0 + manager: conda + platform: linux-aarch64 + dependencies: + _openmp_mutex: '>=4.5' + joblib: '>=1.2.0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + numpy: '>=1.26.3,<2.0a0' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + scipy: '' + threadpoolctl: '>=2.0.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/scikit-learn-1.4.0-py312ha513981_0.conda + hash: + md5: 00f272e7d6be0dab0da6e670fc254910 + sha256: 89d67a7cd2a949144521b59da33c70ccc9c3d36fa083b92e374b5a4069de5552 + category: main + optional: false +- name: scipy + version: 1.12.0 + manager: conda + platform: linux-aarch64 + dependencies: + libblas: '>=3.9.0,<4.0a0' + libcblas: '>=3.9.0,<4.0a0' + libgcc-ng: '>=12' + libgfortran-ng: '' + libgfortran5: '>=12.3.0' + liblapack: '>=3.9.0,<4.0a0' + libstdcxx-ng: '>=12' + numpy: '>=1.26.3,<2.0a0' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/scipy-1.12.0-py312h470d778_2.conda + hash: + md5: 368ed86b1c7790f56228d2aeb98fcc29 + sha256: 8005ac67cdfeafba7a68da4e889dccb17c5ebec6c18400cf7d1dda932effe7c1 + category: main + optional: false +- name: setuptools + version: 69.0.3 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/setuptools-69.0.3-pyhd8ed1ab_0.conda + hash: + md5: 40695fdfd15a92121ed2922900d0308b + sha256: 0fe2a0473ad03dac6c7f5c42ef36a8e90673c88a0350dfefdea4b08d43803db2 + category: main + optional: false +- name: shapely + version: 2.0.2 + manager: conda + platform: linux-aarch64 + dependencies: + geos: '>=3.12.1,<3.12.2.0a0' + libgcc-ng: '>=12' + numpy: '>=1.26.0,<2.0a0' + python: '>=3.12,<3.13.0a0' + python_abi: 3.12.* + url: https://conda.anaconda.org/conda-forge/linux-aarch64/shapely-2.0.2-py312h15622cc_1.conda + hash: + md5: 8c8d051410bc0c92c6756d3dbf7252d5 + sha256: e19ad312547f9478292b8272739f99035c30b6217adaa5a33561632f7f330dd4 + category: main + optional: false +- name: six + version: 1.16.0 + manager: conda + platform: linux-aarch64 + dependencies: + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 + hash: + md5: e5f25f8dbc060e9a8d912e432202afc2 + sha256: a85c38227b446f42c5b90d9b642f2c0567880c15d72492d8da074a59c8f91dd6 + category: main + optional: false +- name: snappy + version: 1.1.10 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/snappy-1.1.10-he8610fa_0.conda + hash: + md5: 11c25e55894bb8207a81a87e6a32b6e7 + sha256: 5a7d6cf781cbaaea4effce4d8f2677cd6173af5e8b744912e1283a704eb91946 + category: main + optional: false +- name: sqlite + version: 3.45.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libsqlite: 3.45.1 + libzlib: '>=1.2.13,<1.3.0a0' + ncurses: '>=6.4,<7.0a0' + readline: '>=8.2,<9.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/sqlite-3.45.1-h3b3482f_0.conda + hash: + md5: f48ce4824eeb9f8a383eb03e82e1ec77 + sha256: 44cbba6ba3a33ca7c53ad48b6d69084007658ee11bc389a087410faa2ff0b723 + category: main + optional: false +- name: threadpoolctl + version: 3.2.0 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.2.0-pyha21a80b_0.conda + hash: + md5: 978d03388b62173b8e6f79162cf52b86 + sha256: 15e2f916fbfe3cc480160aa99eb6ba3edc183fceb234f10151d63870fdc4eccd + category: main + optional: false +- name: tiledb + version: 2.19.1 + manager: conda + platform: linux-aarch64 + dependencies: + azure-core-cpp: '>=1.10.3,<1.11.0a0' + azure-storage-blobs-cpp: '>=12.10.0,<13.0a0' + azure-storage-common-cpp: '>=12.5.0,<13.0a0' + bzip2: '>=1.0.8,<2.0a0' + libabseil: '>=20230802.1,<20230803.0a0' + libcurl: '>=8.5.0,<9.0a0' + libgcc-ng: '>=12' + libgoogle-cloud: '>=2.12.0,<2.13.0a0' + libstdcxx-ng: '>=12' + libxml2: '>=2.12.4,<3.0a0' + libzlib: '>=1.2.13,<1.3.0a0' + lz4-c: '>=1.9.3,<1.10.0a0' + openssl: '>=3.2.0,<4.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/tiledb-2.19.1-hf61e980_0.conda + hash: + md5: 5b3a763d5471858249ba058cd137876a + sha256: f76350d6bcab9c54ac187dd5c97969105ad9dbcba436fb0d37d95b1d625676b2 + category: main + optional: false +- name: tk + version: 8.6.13 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda + hash: + md5: f75105e0585851f818e0009dd1dde4dc + sha256: 7fa27cc512d3a783f38bd16bbbffc008807372499d5b65d089a8e43bde9db267 + category: main + optional: false +- name: tzcode + version: 2024a + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/tzcode-2024a-h31becfc_0.conda + hash: + md5: d7691e522a386b757332784ee7f9906f + sha256: f1e81a576aa69a06fcd6191f6994af6f6d0bc2f5f7df2098d870c492ef11d1ed + category: main + optional: false +- name: tzdata + version: 2024a + manager: conda + platform: linux-aarch64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + hash: + md5: 161081fc7cec0bfda0d86d7cb595f8d8 + sha256: 7b2b69c54ec62a243eb6fba2391b5e443421608c3ae5dbff938ad33ca8db5122 + category: main + optional: false +- name: ucx + version: 1.15.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libnuma: '>=2.0.16,<3.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/ucx-1.15.0-hedb98eb_3.conda + hash: + md5: 6ac7b71587da701842bac2e3061a833e + sha256: 58d94372f86f49ffb422e3b4073eb418298a1ba99a22e02472f6f35b748799c3 + category: main + optional: false +- name: uriparser + version: 0.9.7 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/uriparser-0.9.7-hd600fc2_1.conda + hash: + md5: 72c24e7cfeeecdc48f0f8816b651796f + sha256: 879cd1f3b14eb9984c718ca5607ffc08d96efa891931aa41bad38af4f0b38ebf + category: main + optional: false +- name: urllib3 + version: 2.2.0 + manager: conda + platform: linux-aarch64 + dependencies: + brotli-python: '>=1.0.9' + pysocks: '>=1.5.6,<2.0,!=1.5.7' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.0-pyhd8ed1ab_0.conda + hash: + md5: 6a7e0694921f668a030d52f0c47baebd + sha256: 61a8a3bd36d235c349aedaf1aa6a79cce15d6fe89dca4bb593b596d0211513c6 + category: main + optional: false +- name: xerces-c + version: 3.2.5 + manager: conda + platform: linux-aarch64 + dependencies: + icu: '>=73.2,<74.0a0' + libcurl: '>=8.5.0,<9.0a0' + libgcc-ng: '>=12' + libnsl: '>=2.0.1,<2.1.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xerces-c-3.2.5-hf13c1fb_0.conda + hash: + md5: 5c6a84e179f9fc7f8e0890c28704a8ce + sha256: 6e64e9dc8d9f8bee4bdef16e946be658da3744e40fdd5ca881ac2219a1aba479 + category: main + optional: false +- name: xorg-kbproto + version: 1.0.7 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=9.3.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-kbproto-1.0.7-h3557bc0_1002.tar.bz2 + hash: + md5: ec8ce6b3dac3945a4010559a6284b755 + sha256: 421c0a115b31f02082f95c8f06dbba48b2274718f66a72d64d5102141e5a8731 + category: main + optional: false +- name: xorg-libice + version: 1.1.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.1-h7935292_0.conda + hash: + md5: 025968e2637bca910b9b3e7f6743beff + sha256: c889673c9313798372bea7c93640e853561bda5ba361b265ad4b14d7d1295235 + category: main + optional: false +- name: xorg-libsm + version: 1.2.4 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libuuid: '>=2.38.1,<3.0a0' + xorg-libice: '>=1.1.1,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.4-h5a01bc2_0.conda + hash: + md5: d788eca20ecd63bad8eea7219e5c5fb7 + sha256: 2678975d4001f1123752ceabf9e2810cab51f740624320077de1ab12b537b498 + category: main + optional: false +- name: xorg-libx11 + version: 1.8.7 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libxcb: '>=1.15,<1.16.0a0' + xorg-kbproto: '' + xorg-xextproto: '>=7.3.0,<8.0a0' + xorg-xproto: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.7-h055a233_0.conda + hash: + md5: b3ff774afbb2cd7678044e1fed24f59e + sha256: dc688480f6afa3b5880b9d4e11e25e6c8dd10e961a07cfa30b2dc5e529e250bb + category: main + optional: false +- name: xorg-libxau + version: 1.0.11 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.11-h31becfc_0.conda + hash: + md5: 13de34f69cb73165dbe08c1e9148bedb + sha256: c00a8909e783ba7f4ada7256f0385ae46fc21322f4090fa396c80b4481abd5f4 + category: main + optional: false +- name: xorg-libxdmcp + version: 1.1.3 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=9.3.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.3-h3557bc0_0.tar.bz2 + hash: + md5: a6c9016ae1ca5c47a3603ed4cd65fedd + sha256: 2aad9a0b57796170b8fb40317598fd79cfc7ae27fa7fb68c417d815e44499d59 + category: main + optional: false +- name: xorg-libxext + version: 1.3.4 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + xorg-libx11: '>=1.7.2,<2.0a0' + xorg-xextproto: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.4-h2a766a3_2.conda + hash: + md5: 0cea7d840c8eeaa4e349e0b4775c826d + sha256: 16eff29fb70b2f89b9120d112d2d5df1bf7bd4e95d1e5baafabc61dac4977fa8 + category: main + optional: false +- name: xorg-libxrender + version: 0.9.11 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + xorg-libx11: '>=1.8.6,<2.0a0' + xorg-renderproto: '' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.11-h7935292_0.conda + hash: + md5: 8c96b84f7fb97a3cd533a14dbdcd6626 + sha256: 15ab433c3b565d92bbd9dc83e469bb4ff1076f9002f7cd142b8a39e1b6cbcfab + category: main + optional: false +- name: xorg-renderproto + version: 0.11.1 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=9.3.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-renderproto-0.11.1-h3557bc0_1002.tar.bz2 + hash: + md5: 01cbfe96ce66b78a9a270ac305791dd2 + sha256: e57e8b4a58f8c3b5011bf6cd66f499fca9fc5067981bb33f828750b168c3698d + category: main + optional: false +- name: xorg-xextproto + version: 7.3.0 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xextproto-7.3.0-h2a766a3_1003.conda + hash: + md5: 32de1e4422c986e3b6eff59e7edc4d04 + sha256: 62298f1c7b963f3a5921a65d9cb6aae82c3ec8b3069319c8264c5b0a3d190286 + category: main + optional: false +- name: xorg-xproto + version: 7.0.31 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=9.3.0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xproto-7.0.31-h3557bc0_1007.tar.bz2 + hash: + md5: 987e98faa0ad2c667bbea6b6aae260bc + sha256: 7711ca1898e6f74a8434931fe6c0593ff7201277778aa09ea012d8be8bc7a7f5 + category: main + optional: false +- name: xyzservices + version: 2023.10.1 + manager: conda + platform: linux-aarch64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2023.10.1-pyhd8ed1ab_0.conda + hash: + md5: 1e0d85c0e2fef9539218da185b285f54 + sha256: da655e2e0a742fddefeeaf2dd828b62a1820a3755d13341e1a555a10fcb9cf81 + category: main + optional: false +- name: xz + version: 5.2.6 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.2.6-h9cdd2b7_0.tar.bz2 + hash: + md5: 83baad393a31d59c20b63ba4da6592df + sha256: 93f58a7b393adf41fa007ac8c55978765e957e90cd31877ece1e5a343cb98220 + category: main + optional: false +- name: zlib + version: 1.2.13 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libzlib: 1.2.13 + url: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.2.13-h31becfc_5.conda + hash: + md5: 96866c7301479abaf8308c50958c71a4 + sha256: aa3e9d46b13d1959faf634f03d929d7dec950dc1b84a8ff109f7f0e3f364b562 + category: main + optional: false +- name: zstd + version: 1.5.5 + manager: conda + platform: linux-aarch64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.5-h4c53e97_0.conda + hash: + md5: b74eb9dbb5c3c15cb3cee7cbdf198c75 + sha256: d1e070029e9d07a3f25e6ed082d507b0f3cff1b109dd18d0b091a5c7b86dd07b + category: main + optional: false diff --git a/targets/slideruleearth-aws/Makefile b/targets/slideruleearth-aws/Makefile index cbe4c32b5..5203ad93e 100644 --- a/targets/slideruleearth-aws/Makefile +++ b/targets/slideruleearth-aws/Makefile @@ -191,7 +191,7 @@ manager-destroy: ## destroy manager using terraform; needs DOMAIN cd terraform/manager && terraform destroy container-runtime-python-lock: ## create the lock file for the container runtime environment base python image -# cd ../python-container && conda-lock -p linux-aarch64 -f environment.yml + cd ../python-container && conda-lock -p linux-aarch64 -f environment.yml cd ../python-container && conda-lock render -p linux-aarch64 container-runtime-python-docker: ## create the container runtime environment base python image diff --git a/targets/slideruleearth-aws/config.json b/targets/slideruleearth-aws/config.json index ac8118f04..ed0d29d98 100644 --- a/targets/slideruleearth-aws/config.json +++ b/targets/slideruleearth-aws/config.json @@ -8,5 +8,6 @@ "authenticate_to_lpdaac": true, "authenticate_to_podaac": true, "register_as_service": true, - "authenticate_to_ps": false + "authenticate_to_ps": false, + "container_registry": "742127912612.dkr.ecr.us-west-2.amazonaws.com" } \ No newline at end of file From 878a48b4474e6a6a7b598e4abdddcceb8b438697 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Wed, 14 Feb 2024 17:01:30 +0000 Subject: [PATCH 06/43] added option to rethrow exceptions to icesat2 module --- clients/python/sliderule/icesat2.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/clients/python/sliderule/icesat2.py b/clients/python/sliderule/icesat2.py index 9f7db843d..36ced765f 100644 --- a/clients/python/sliderule/icesat2.py +++ b/clients/python/sliderule/icesat2.py @@ -41,6 +41,9 @@ # profiling times for each major function profiles = {} +# whether exceptions should be rethrown +rethrow_exceptions = False + # icesat2 parameters CNF_POSSIBLE_TEP = -2 CNF_NOT_CONSIDERED = -1 @@ -285,7 +288,7 @@ def __build_request(parm, resources, default_asset='icesat2'): # # Initialize # -def init (url=sliderule.service_url, verbose=False, max_resources=earthdata.DEFAULT_MAX_REQUESTED_RESOURCES, loglevel=logging.CRITICAL, organization=sliderule.service_org, desired_nodes=None, time_to_live=60, bypass_dns=False): +def init (url=sliderule.service_url, verbose=False, max_resources=earthdata.DEFAULT_MAX_REQUESTED_RESOURCES, loglevel=logging.CRITICAL, organization=sliderule.service_org, desired_nodes=None, time_to_live=60, bypass_dns=False, rethrow=False): ''' Initializes the Python client for use with SlideRule and should be called before other ICESat-2 API calls. This function is a wrapper for the `sliderule.init(...) function `_. @@ -300,8 +303,10 @@ def init (url=sliderule.service_url, verbose=False, max_resources=earthdata.DEFA >>> from sliderule import icesat2 >>> icesat2.init() ''' + global rethrow_exceptions sliderule.init(url, verbose, loglevel, organization, desired_nodes, time_to_live, bypass_dns, plugins=['icesat2']) earthdata.set_max_resources(max_resources) # set maximum number of resources allowed per request + rethrow_exceptions = rethrow # # ATL06 @@ -403,7 +408,11 @@ def atl06p(parm, callbacks={}, resources=None, keep_id=False, as_numpy_array=Fal # Handle Runtime Errors except RuntimeError as e: logger.critical(e) - return sliderule.emptyframe() + if rethrow_exceptions: + raise + + # Error Case + return sliderule.emptyframe() # # Subsetted ATL06 @@ -480,7 +489,11 @@ def atl06sp(parm, callbacks={}, resources=None, keep_id=False, as_numpy_array=Fa # Handle Runtime Errorss except RuntimeError as e: logger.critical(e) - return sliderule.emptyframe() + if rethrow_exceptions: + raise + + # Error Case + return sliderule.emptyframe() # # Subsetted ATL03 @@ -639,8 +652,10 @@ def atl03sp(parm, callbacks={}, resources=None, keep_id=False, height_key=None): # Handle Runtime Errors except RuntimeError as e: logger.critical(e) + if rethrow_exceptions: + raise - # Error or No Data + # Error Case return sliderule.emptyframe() # @@ -719,4 +734,8 @@ def atl08p(parm, callbacks={}, resources=None, keep_id=False, as_numpy_array=Fal # Handle Runtime Errors except RuntimeError as e: logger.critical(e) - return sliderule.emptyframe() + if rethrow_exceptions: + raise + + # Error Case + return sliderule.emptyframe() From ba97aee35e8727a6a5ca3f1b674724cbc2a09864 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 15 Feb 2024 14:45:36 +0000 Subject: [PATCH 07/43] added region to atl03 record --- plugins/icesat2/plugin/Atl03Reader.cpp | 3 ++- plugins/icesat2/plugin/Atl03Reader.h | 2 +- plugins/icesat2/plugin/Atl06Dispatch.cpp | 13 ++++--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/plugins/icesat2/plugin/Atl03Reader.cpp b/plugins/icesat2/plugin/Atl03Reader.cpp index 7fa9369b9..98346ec03 100644 --- a/plugins/icesat2/plugin/Atl03Reader.cpp +++ b/plugins/icesat2/plugin/Atl03Reader.cpp @@ -64,6 +64,7 @@ const RecordObject::fieldDef_t Atl03Reader::phRecDef[] = { const char* Atl03Reader::exRecType = "atl03rec"; const RecordObject::fieldDef_t Atl03Reader::exRecDef[] = { + {"region", RecordObject::UINT8, offsetof(extent_t, region), 1, NULL, NATIVE_FLAGS}, {"track", RecordObject::UINT8, offsetof(extent_t, track), 1, NULL, NATIVE_FLAGS}, {"pair", RecordObject::UINT8, offsetof(extent_t, pair), 1, NULL, NATIVE_FLAGS}, {"sc_orient", RecordObject::UINT8, offsetof(extent_t, spacecraft_orientation), 1, NULL, NATIVE_FLAGS}, @@ -1629,8 +1630,8 @@ void Atl03Reader::generateExtentRecord (uint64_t extent_id, info_t* info, TrackS /* Allocate and Initialize Extent Record */ RecordObject* record = new RecordObject(exRecType, extent_bytes); extent_t* extent = (extent_t*)record->getRecordData(); - extent->valid = state.extent_valid; extent->extent_id = extent_id; + extent->region = start_region; extent->track = info->track; extent->pair = info->pair; extent->spacecraft_orientation = atl03.sc_orient[0]; diff --git a/plugins/icesat2/plugin/Atl03Reader.h b/plugins/icesat2/plugin/Atl03Reader.h index 0d89b7cc5..2fa1a749d 100644 --- a/plugins/icesat2/plugin/Atl03Reader.h +++ b/plugins/icesat2/plugin/Atl03Reader.h @@ -97,7 +97,7 @@ class Atl03Reader: public LuaObject /* Extent Record */ typedef struct { - bool valid; + uint8_t region; uint8_t track; // 1, 2, or 3 uint8_t pair; // 0 (l), 1 (r) uint8_t spacecraft_orientation; // sc_orient_t diff --git a/plugins/icesat2/plugin/Atl06Dispatch.cpp b/plugins/icesat2/plugin/Atl06Dispatch.cpp index 7c1027bba..0df79601e 100644 --- a/plugins/icesat2/plugin/Atl06Dispatch.cpp +++ b/plugins/icesat2/plugin/Atl06Dispatch.cpp @@ -316,19 +316,14 @@ bool Atl06Dispatch::processTermination (void) void Atl06Dispatch::iterativeFitStage (Atl03Reader::extent_t* extent, result_t& result) { /* Check Valid Extent */ - if(extent->valid && result.elevation.photon_count > 0) + if(result.elevation.photon_count <= 0) { - result.provided = true; - } - else - { - // the check for photon count is redundent with the check for - // a valid extent, but given that the code below is invalid - // if the number of photons is less than or equal to zero, - // the check is provided explicitly return; } + /* Result is Provided */ + result.provided = true; + /* Initial Conditions */ bool done = false; bool invalid = false; From 0b00277df7c040cc4fe06abf7eadd4a665d0d76f Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 15 Feb 2024 14:46:05 +0000 Subject: [PATCH 08/43] fixed metric endpoint to return valid prometheus table --- packages/core/LuaLibrarySys.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/core/LuaLibrarySys.cpp b/packages/core/LuaLibrarySys.cpp index 486c0dc24..d98e1f8c9 100644 --- a/packages/core/LuaLibrarySys.cpp +++ b/packages/core/LuaLibrarySys.cpp @@ -228,13 +228,26 @@ int LuaLibrarySys::lsys_log (lua_State* L) } /*---------------------------------------------------------------------------- - * lsys_metric - .metric(<...) + * lsys_metric - .metric() *----------------------------------------------------------------------------*/ int LuaLibrarySys::lsys_metric (lua_State* L) -//TODO: need to populate with metrics... { lua_newtable(L); - LuaEngine::setAttrInt(L, "alive", 1); + + /* Alive */ + lua_pushstring(L, "alive"); + lua_newtable(L); + { + lua_pushstring(L, "value"); + lua_pushnumber(L, 1); + lua_settable(L, -3); + + lua_pushstring(L, "type"); + lua_pushstring(L, EventLib::subtype2str(EventLib::GAUGE)); + lua_settable(L, -3); + } + lua_settable(L, -3); + return 1; } From 5f72e1df477c2ef9e46f7f3018eff105c9a61490 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 15 Feb 2024 14:46:23 +0000 Subject: [PATCH 09/43] fixed file leakage with parquet s3 upload --- packages/arrow/ParquetBuilder.cpp | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index 36667f375..1963acb5e 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -699,17 +699,22 @@ void* ParquetBuilder::builderThread(void* parm) (_path[3] == '/') && (_path[4] == '/')) { - #ifdef __aws__ + /* Upload File to S3 */ builder->send2S3(&_path[5]); - #else - LuaEndpoint::generateExceptionStatus(RTE_ERROR, CRITICAL, outQ, NULL, "Output path specifies S3, but server compiled without AWS support"); - #endif } else { - /* Stream Back to Client */ + /* Stream File Back to Client */ builder->send2Client(); } + + /* Remove File */ + int rc = remove(builder->fileName); + if(rc != 0) + { + mlog(CRITICAL, "Failed (%d) to delete file %s: %s", rc, builder->fileName, strerror(errno)); + } + stop_trace(INFO, send_trace_id); /* Signal Completion */ @@ -1250,6 +1255,7 @@ bool ParquetBuilder::send2S3 (const char* s3dst) return status; #else + LuaEndpoint::generateExceptionStatus(RTE_ERROR, CRITICAL, outQ, NULL, "Output path specifies S3, but server compiled without AWS support"); return false; #endif } @@ -1301,19 +1307,11 @@ bool ParquetBuilder::send2Client (void) } while(false); /* Close File */ - int rc1 = fclose(fp); - if(rc1 != 0) - { - status = false; - mlog(CRITICAL, "Failed (%d) to close file %s: %s", rc1, fileName, strerror(errno)); - } - - /* Remove File */ - int rc2 = remove(fileName); - if(rc2 != 0) + int rc = fclose(fp); + if(rc != 0) { status = false; - mlog(CRITICAL, "Failed (%d) to delete file %s: %s", rc2, fileName, strerror(errno)); + mlog(CRITICAL, "Failed (%d) to close file %s: %s", rc, fileName, strerror(errno)); } } else // unable to open file From 08b56f70880d9b86ace32f318805357a5d603193 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 15 Feb 2024 14:46:51 +0000 Subject: [PATCH 10/43] fixed disk usage metric to match t4g instance type disk volume --- .../docker/monitor/dashboard-sliderule-node-sys-metrics.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/targets/slideruleearth-aws/docker/monitor/dashboard-sliderule-node-sys-metrics.json b/targets/slideruleearth-aws/docker/monitor/dashboard-sliderule-node-sys-metrics.json index 8f53000b6..9e78454e1 100644 --- a/targets/slideruleearth-aws/docker/monitor/dashboard-sliderule-node-sys-metrics.json +++ b/targets/slideruleearth-aws/docker/monitor/dashboard-sliderule-node-sys-metrics.json @@ -783,7 +783,7 @@ "targets": [ { "exemplar": true, - "expr": "node_filesystem_avail_bytes{device=\"/dev/root\",job=\"sliderule_node_sys\"}", + "expr": "node_filesystem_avail_bytes{device=\"/dev/nvme0n1p1\",job=\"sliderule_node_sys\"}", "interval": "", "legendFormat": "", "refId": "A" From af7093b06b1e3534064acd3632996b861b3029a2 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 15 Feb 2024 21:38:03 +0000 Subject: [PATCH 11/43] added region to atl03 and atl06 record; added aux flag to record definitions to provide option for faster decode in python --- clients/python/sliderule/sliderule.py | 29 +++++++++++++++++++++++ clients/python/utils/benchmark.py | 5 ++++ packages/core/RecordObject.cpp | 2 ++ packages/core/RecordObject.h | 3 ++- plugins/icesat2/plugin/Atl03Reader.cpp | 30 ++++++++++++------------ plugins/icesat2/plugin/Atl06Dispatch.cpp | 22 +++++++++-------- plugins/icesat2/plugin/Atl06Dispatch.h | 3 ++- 7 files changed, 67 insertions(+), 27 deletions(-) diff --git a/clients/python/sliderule/sliderule.py b/clients/python/sliderule/sliderule.py index 1e9f35ef4..8a67db27b 100644 --- a/clients/python/sliderule/sliderule.py +++ b/clients/python/sliderule/sliderule.py @@ -71,6 +71,7 @@ MAX_PS_CLUSTER_WAIT_SECS = 600 request_timeout = (10, 120) # (connection, read) in seconds +decode_aux = True logger = logging.getLogger(__name__) console = None @@ -232,6 +233,10 @@ def __decode_native(rectype, rawdata): if "PTR" in flags: continue + # check for mvp flag + if not decode_aux and "AUX" in flags: + continue + # get endianness if "LE" in flags: endian = '<' @@ -883,6 +888,30 @@ def set_rqst_timeout (timeout): else: raise FatalError('timeout must be a tuple (, )') +# +# set_processing_flags +# +def set_processing_flags (aux=True): + ''' + Sets flags used when processing the record definitions + + Parameters + ---------- + aux: bool + decode auxiliary fields + + Examples + -------- + >>> import sliderule + >>> sliderule.set_processing_flags(aux=False) + ''' + global decode_aux + if type(aux) == bool: + decode_aux = aux + else: + raise FatalError('aux must be a boolean') + + # # update_available_servers # diff --git a/clients/python/utils/benchmark.py b/clients/python/utils/benchmark.py index 6ed69d9f2..08429ba0f 100644 --- a/clients/python/utils/benchmark.py +++ b/clients/python/utils/benchmark.py @@ -48,6 +48,7 @@ parser.add_argument('--verbose', '-v', action='store_true', default=False) parser.add_argument('--loglvl', '-j', type=str, default="CRITICAL") parser.add_argument('--nocleanup', '-u', action='store_true', default=False) +parser.add_argument('--no_aux', '-x', action='store_true', default=False) args,_ = parser.parse_known_args() # Initialize Organization @@ -59,6 +60,10 @@ # Initialize SlideRule Client sliderule.init(args.domain, verbose=args.verbose, loglevel=args.loglvl, organization=args.organization, desired_nodes=args.desired_nodes, time_to_live=args.time_to_live) +# Configure SlideRule Client +if args.no_aux: + sliderule.set_processing_flags(aux=False) + # Generate Region Polygon region = sliderule.toregion(args.aoi) diff --git a/packages/core/RecordObject.cpp b/packages/core/RecordObject.cpp index e6295374f..8fc9780c3 100644 --- a/packages/core/RecordObject.cpp +++ b/packages/core/RecordObject.cpp @@ -1168,6 +1168,8 @@ const char* RecordObject::flags2str (unsigned int flags) if(flags & POINTER) flagss += "|PTR"; + if(flags & AUX) flagss += "|AUX"; + return StringLib::duplicate(flagss.c_str()); } diff --git a/packages/core/RecordObject.h b/packages/core/RecordObject.h index 8dd6ff44f..a817f452f 100644 --- a/packages/core/RecordObject.h +++ b/packages/core/RecordObject.h @@ -108,7 +108,8 @@ class RecordObject typedef enum { BIGENDIAN = 0x00000001, POINTER = 0x00000002, - BATCH = 0x00000004 + BATCH = 0x00000004, // batch record + AUX = 0x00000008 // auxiliary field } fieldFlags_t; typedef struct { diff --git a/plugins/icesat2/plugin/Atl03Reader.cpp b/plugins/icesat2/plugin/Atl03Reader.cpp index 98346ec03..f8bf84935 100644 --- a/plugins/icesat2/plugin/Atl03Reader.cpp +++ b/plugins/icesat2/plugin/Atl03Reader.cpp @@ -53,27 +53,27 @@ const RecordObject::fieldDef_t Atl03Reader::phRecDef[] = { {"x_atc", RecordObject::FLOAT, offsetof(photon_t, x_atc), 1, NULL, NATIVE_FLAGS}, {"y_atc", RecordObject::FLOAT, offsetof(photon_t, y_atc), 1, NULL, NATIVE_FLAGS}, {"height", RecordObject::FLOAT, offsetof(photon_t, height), 1, NULL, NATIVE_FLAGS}, - {"relief", RecordObject::FLOAT, offsetof(photon_t, relief), 1, NULL, NATIVE_FLAGS}, - {"landcover", RecordObject::UINT8, offsetof(photon_t, landcover), 1, NULL, NATIVE_FLAGS}, - {"snowcover", RecordObject::UINT8, offsetof(photon_t, snowcover), 1, NULL, NATIVE_FLAGS}, - {"atl08_class", RecordObject::UINT8, offsetof(photon_t, atl08_class), 1, NULL, NATIVE_FLAGS}, - {"atl03_cnf", RecordObject::INT8, offsetof(photon_t, atl03_cnf), 1, NULL, NATIVE_FLAGS}, - {"quality_ph", RecordObject::INT8, offsetof(photon_t, quality_ph), 1, NULL, NATIVE_FLAGS}, - {"yapc_score", RecordObject::UINT8, offsetof(photon_t, yapc_score), 1, NULL, NATIVE_FLAGS} + {"relief", RecordObject::FLOAT, offsetof(photon_t, relief), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"landcover", RecordObject::UINT8, offsetof(photon_t, landcover), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"snowcover", RecordObject::UINT8, offsetof(photon_t, snowcover), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"atl08_class", RecordObject::UINT8, offsetof(photon_t, atl08_class), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"atl03_cnf", RecordObject::INT8, offsetof(photon_t, atl03_cnf), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"quality_ph", RecordObject::INT8, offsetof(photon_t, quality_ph), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"yapc_score", RecordObject::UINT8, offsetof(photon_t, yapc_score), 1, NULL, NATIVE_FLAGS | RecordObject::AUX} }; const char* Atl03Reader::exRecType = "atl03rec"; const RecordObject::fieldDef_t Atl03Reader::exRecDef[] = { - {"region", RecordObject::UINT8, offsetof(extent_t, region), 1, NULL, NATIVE_FLAGS}, - {"track", RecordObject::UINT8, offsetof(extent_t, track), 1, NULL, NATIVE_FLAGS}, - {"pair", RecordObject::UINT8, offsetof(extent_t, pair), 1, NULL, NATIVE_FLAGS}, - {"sc_orient", RecordObject::UINT8, offsetof(extent_t, spacecraft_orientation), 1, NULL, NATIVE_FLAGS}, - {"rgt", RecordObject::UINT16, offsetof(extent_t, reference_ground_track), 1, NULL, NATIVE_FLAGS}, - {"cycle", RecordObject::UINT16, offsetof(extent_t, cycle), 1, NULL, NATIVE_FLAGS}, + {"region", RecordObject::UINT8, offsetof(extent_t, region), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"track", RecordObject::UINT8, offsetof(extent_t, track), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"pair", RecordObject::UINT8, offsetof(extent_t, pair), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"sc_orient", RecordObject::UINT8, offsetof(extent_t, spacecraft_orientation), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"rgt", RecordObject::UINT16, offsetof(extent_t, reference_ground_track), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"cycle", RecordObject::UINT16, offsetof(extent_t, cycle), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"segment_id", RecordObject::UINT32, offsetof(extent_t, segment_id), 1, NULL, NATIVE_FLAGS}, {"segment_dist", RecordObject::DOUBLE, offsetof(extent_t, segment_distance), 1, NULL, NATIVE_FLAGS}, // distance from equator - {"background_rate", RecordObject::DOUBLE, offsetof(extent_t, background_rate), 1, NULL, NATIVE_FLAGS}, - {"solar_elevation", RecordObject::FLOAT, offsetof(extent_t, solar_elevation), 1, NULL, NATIVE_FLAGS}, + {"background_rate", RecordObject::DOUBLE, offsetof(extent_t, background_rate), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"solar_elevation", RecordObject::FLOAT, offsetof(extent_t, solar_elevation), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"extent_id", RecordObject::UINT64, offsetof(extent_t, extent_id), 1, NULL, NATIVE_FLAGS}, {"photons", RecordObject::USER, offsetof(extent_t, photons), 0, phRecType, NATIVE_FLAGS | RecordObject::BATCH} // variable length }; diff --git a/plugins/icesat2/plugin/Atl06Dispatch.cpp b/plugins/icesat2/plugin/Atl06Dispatch.cpp index 0df79601e..f87e95667 100644 --- a/plugins/icesat2/plugin/Atl06Dispatch.cpp +++ b/plugins/icesat2/plugin/Atl06Dispatch.cpp @@ -87,22 +87,23 @@ const char* Atl06Dispatch::elRecType = "atl06rec.elevation"; // extended elevati const RecordObject::fieldDef_t Atl06Dispatch::elRecDef[] = { {"extent_id", RecordObject::UINT64, offsetof(elevation_t, extent_id), 1, NULL, NATIVE_FLAGS}, {"segment_id", RecordObject::UINT32, offsetof(elevation_t, segment_id), 1, NULL, NATIVE_FLAGS}, - {"n_fit_photons", RecordObject::INT32, offsetof(elevation_t, photon_count), 1, NULL, NATIVE_FLAGS}, - {"pflags", RecordObject::UINT16, offsetof(elevation_t, pflags), 1, NULL, NATIVE_FLAGS}, - {"rgt", RecordObject::UINT16, offsetof(elevation_t, rgt), 1, NULL, NATIVE_FLAGS}, - {"cycle", RecordObject::UINT16, offsetof(elevation_t, cycle), 1, NULL, NATIVE_FLAGS}, - {"spot", RecordObject::UINT8, offsetof(elevation_t, spot), 1, NULL, NATIVE_FLAGS}, - {"gt", RecordObject::UINT8, offsetof(elevation_t, gt), 1, NULL, NATIVE_FLAGS}, + {"n_fit_photons", RecordObject::INT32, offsetof(elevation_t, photon_count), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"pflags", RecordObject::UINT16, offsetof(elevation_t, pflags), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"rgt", RecordObject::UINT16, offsetof(elevation_t, rgt), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"cycle", RecordObject::UINT8, offsetof(elevation_t, cycle), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"region", RecordObject::UINT8, offsetof(elevation_t, region), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"spot", RecordObject::UINT8, offsetof(elevation_t, spot), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"gt", RecordObject::UINT8, offsetof(elevation_t, gt), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"time", RecordObject::TIME8, offsetof(elevation_t, time_ns), 1, NULL, NATIVE_FLAGS}, {"latitude", RecordObject::DOUBLE, offsetof(elevation_t, latitude), 1, NULL, NATIVE_FLAGS}, {"longitude", RecordObject::DOUBLE, offsetof(elevation_t, longitude), 1, NULL, NATIVE_FLAGS}, {"h_mean", RecordObject::DOUBLE, offsetof(elevation_t, h_mean), 1, NULL, NATIVE_FLAGS}, - {"dh_fit_dx", RecordObject::FLOAT, offsetof(elevation_t, dh_fit_dx), 1, NULL, NATIVE_FLAGS}, + {"dh_fit_dx", RecordObject::FLOAT, offsetof(elevation_t, dh_fit_dx), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"x_atc", RecordObject::FLOAT, offsetof(elevation_t, x_atc), 1, NULL, NATIVE_FLAGS}, {"y_atc", RecordObject::FLOAT, offsetof(elevation_t, y_atc), 1, NULL, NATIVE_FLAGS}, - {"w_surface_window_final", RecordObject::FLOAT, offsetof(elevation_t, window_height), 1, NULL, NATIVE_FLAGS}, - {"rms_misfit", RecordObject::FLOAT, offsetof(elevation_t, rms_misfit), 1, NULL, NATIVE_FLAGS}, - {"h_sigma", RecordObject::FLOAT, offsetof(elevation_t, h_sigma), 1, NULL, NATIVE_FLAGS} + {"w_surface_window_final", RecordObject::FLOAT, offsetof(elevation_t, window_height), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"rms_misfit", RecordObject::FLOAT, offsetof(elevation_t, rms_misfit), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"h_sigma", RecordObject::FLOAT, offsetof(elevation_t, h_sigma), 1, NULL, NATIVE_FLAGS | RecordObject::AUX} }; const char* Atl06Dispatch::atRecType = "atl06rec"; @@ -248,6 +249,7 @@ bool Atl06Dispatch::processRecord (RecordObject* record, okey_t key, recVec_t* r result.elevation.segment_id = extent->segment_id; result.elevation.rgt = extent->reference_ground_track; result.elevation.cycle = extent->cycle; + result.elevation.region = extent->region; result.elevation.x_atc = extent->segment_distance; result.elevation.pflags = 0; diff --git a/plugins/icesat2/plugin/Atl06Dispatch.h b/plugins/icesat2/plugin/Atl06Dispatch.h index 0402a61df..07e752649 100644 --- a/plugins/icesat2/plugin/Atl06Dispatch.h +++ b/plugins/icesat2/plugin/Atl06Dispatch.h @@ -98,7 +98,8 @@ class Atl06Dispatch: public DispatchObject int32_t photon_count; // number of photons used in final elevation calculation uint16_t pflags; // processing flags uint16_t rgt; // reference ground track - uint16_t cycle; // cycle number + uint8_t cycle; // granule cycle number + uint8_t region; // granule region number uint8_t spot; // 1 through 6, or 0 if unknown uint8_t gt; // gt1l, gt1r, gt2l, gt2r, gt3l, gt3r int64_t time_ns; // nanoseconds from GPS epoch From 14a8ffb61a4e7a81abda896d1cd017f2b999e5ed Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Fri, 16 Feb 2024 14:00:57 +0000 Subject: [PATCH 12/43] fix for #368 - atl08 segment index was able to point to one element past land segment array --- plugins/icesat2/plugin/Atl03Reader.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/icesat2/plugin/Atl03Reader.cpp b/plugins/icesat2/plugin/Atl03Reader.cpp index f8bf84935..a0ef0d6a3 100644 --- a/plugins/icesat2/plugin/Atl03Reader.cpp +++ b/plugins/icesat2/plugin/Atl03Reader.cpp @@ -688,11 +688,11 @@ void Atl03Reader::Atl08Class::classify (info_t* info, const Region& region, cons { int32_t atl03_segment = atl03.segment_id[atl03_segment_index]; - /* Get Land and Snow Flags */ + /* Get ATL08 Land Segment Index */ if(phoreal || ancillary) { - while( (atl08_segment_index < segment_id_beg.size) && - ((segment_id_beg[atl08_segment_index] + NUM_ATL03_SEGS_IN_ATL08_SEG) <= atl03_segment) ) + while( (atl08_segment_index < (segment_id_beg.size - 1)) && + (segment_id_beg[atl08_segment_index + 1] <= atl03_segment) ) { atl08_segment_index++; } From 45ca2d77d775c2117a84fc98c1b9edefe8e411f8 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Fri, 16 Feb 2024 17:18:14 +0000 Subject: [PATCH 13/43] python container running and saying hello world --- packages/core/EndpointObject.h | 1 + packages/cre/ContainerRunner.cpp | 69 ++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/packages/core/EndpointObject.h b/packages/core/EndpointObject.h index a7f3cfbbc..41d3184d0 100644 --- a/packages/core/EndpointObject.h +++ b/packages/core/EndpointObject.h @@ -77,6 +77,7 @@ class EndpointObject: public LuaObject typedef enum { OK = 200, Created = 201, + No_Content = 204, Bad_Request = 400, Unauthorized = 401, Not_Found = 404, diff --git a/packages/cre/ContainerRunner.cpp b/packages/cre/ContainerRunner.cpp index 208c47e81..9537a02eb 100644 --- a/packages/cre/ContainerRunner.cpp +++ b/packages/cre/ContainerRunner.cpp @@ -156,7 +156,7 @@ void* ContainerRunner::controlThread (void* parm) /* Build Container Parameters */ FString image("\"Image\": \"%s/%s\"", REGISTRY, cr->parms->image); FString host_config("\"HostConfig\": { \"Binds\": [\"%s:%s\"] }", "/usr/local/share/applications", "/applications"); - FString cmd("\"Cmd\": [\"python\", \"%s\"]}", cr->parms->script); + FString cmd("\"Cmd\": [\"python\", \"/applications/%s\"]}", cr->parms->script); FString data("{%s, %s, %s}", image.c_str(), host_config.c_str(), cmd.c_str()); /* Create Container */ @@ -165,36 +165,43 @@ void* ContainerRunner::controlThread (void* parm) long create_http_code = CurlLib::request(EndpointObject::POST, create_url.c_str(), data.c_str(), &create_response, NULL, false, false, &headers, unix_socket); if(create_http_code != EndpointObject::Created) mlog(CRITICAL, "Failed to create container <%s>: %ld - %s", cr->parms->image, create_http_code, create_response); else mlog(INFO, "Created container <%s>: %s", cr->parms->image, create_response); -// -// /* Wait for Completion and Get Result */ -// if(false && create_http_code == EndpointObject::OK) -// { -// /* Get Container ID */ -// rapidjson::Document json; -// json.Parse(create_response); -// const char* container_id = json["Id"].GetString(); -// -// /* Start Container */ -// FString start_url("http://localhost/%s/containers/%s/start", api_version, container_id); -// const char* start_response = NULL; -// long start_http_code = CurlLib::request(EndpointObject::POST, start_url.c_str(), NULL, &start_response, NULL, false, false, NULL, unix_socket); -// if(start_http_code != EndpointObject::OK) mlog(CRITICAL, "Failed to start container <%s>: %s", cr->parms->image, start_response); -// -// /* Poll Completion of Container */ -// FString wait_url("http://localhost/%s/containers/%s/wait", api_version, container_id); -// const char* wait_response = NULL; -// long wait_http_code = CurlLib::request(EndpointObject::POST, wait_url.c_str(), NULL, &wait_response, NULL, false, false, NULL, unix_socket); -// if(wait_http_code != EndpointObject::OK) mlog(CRITICAL, "Failed to wait for container <%s>: %s", cr->parms->image, wait_response); -// // TODO - need to poll somehow and tie in the timeout -// -// /* Get Result */ -// // from well known file -// -// /* Clean Up */ -// delete [] start_response; -// delete [] wait_response; -// } -// + + /* Wait for Completion and Get Result */ + if(create_http_code == EndpointObject::Created) + { + /* Get Container ID */ + rapidjson::Document json; + json.Parse(create_response); + const char* container_id = json["Id"].GetString(); + + /* Start Container */ + FString start_url("http://localhost/%s/containers/%s/start", api_version, container_id); + const char* start_response = NULL; + long start_http_code = CurlLib::request(EndpointObject::POST, start_url.c_str(), NULL, &start_response, NULL, false, false, NULL, unix_socket); + if(start_http_code != EndpointObject::No_Content) mlog(CRITICAL, "Failed to start container <%s>: %ld - %s", cr->parms->image, start_http_code, start_response); + else mlog(INFO, "Started container <%s> with Id %s: %s\n", cr->parms->image, container_id, start_response); + // TODO - could also generate exception status records with the repsonses when there is an error + + /* Poll Completion of Container */ + FString wait_url("http://localhost/%s/containers/%s/wait", api_version, container_id); + const char* wait_response = NULL; + long wait_http_code = CurlLib::request(EndpointObject::POST, wait_url.c_str(), NULL, &wait_response, NULL, false, false, NULL, unix_socket); + if(wait_http_code != EndpointObject::OK) mlog(CRITICAL, "Failed to wait for container <%s>: %ld - %s", cr->parms->image, wait_http_code, wait_response); + else mlog(INFO, "Waited for container <%s> with Id %s: %s\n", cr->parms->image, container_id, wait_response); + // TODO - could also generate exception status records with the repsonses when there is an error + + /* Remove Container */ + // TODO, need to clean up response below as well + + /* Get Result */ + // read files from output directory (provided to container) + // stream files back to user (or to S3??? like ParquetBuilder; maybe need generic library for that) + + /* Clean Up */ + delete [] start_response; + delete [] wait_response; + } + /* Clean Up */ delete [] create_response; From 542f1fac5a514ef5bc9a6520f7abdd9af8415ceb Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 22 Feb 2024 16:00:30 +0000 Subject: [PATCH 14/43] created alert to replace generateException function; added s3 staging code --- .../slideruleearth-aws/asset_directory.csv | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/targets/slideruleearth-aws/asset_directory.csv b/targets/slideruleearth-aws/asset_directory.csv index 1b61502fc..2cdc0a93e 100644 --- a/targets/slideruleearth-aws/asset_directory.csv +++ b/targets/slideruleearth-aws/asset_directory.csv @@ -1,29 +1,30 @@ -asset, identity, driver, path, index, region, endpoint -icesat2, nsidc-cloud, cumulus, nsidc-cumulus-prod-protected, nil, us-west-2, https://s3.us-west-2.amazonaws.com -icesat2-atl06, nsidc-cloud, cumulus, nsidc-cumulus-prod-protected, nil, us-west-2, https://s3.us-west-2.amazonaws.com -icesat2-atl08, nsidc-cloud, cumulus, nsidc-cumulus-prod-protected, nil, us-west-2, https://s3.us-west-2.amazonaws.com -gedil4a, ornl-cloud, s3, ornl-cumulus-prod-protected/gedi/GEDI_L4A_AGB_Density_V2_1/data, nil, us-west-2, https://s3.us-west-2.amazonaws.com +asset, identity, driver, path, index, region, endpoint +icesat2, nsidc-cloud, cumulus, nsidc-cumulus-prod-protected, nil, us-west-2, https://s3.us-west-2.amazonaws.com +icesat2-atl06, nsidc-cloud, cumulus, nsidc-cumulus-prod-protected, nil, us-west-2, https://s3.us-west-2.amazonaws.com +icesat2-atl08, nsidc-cloud, cumulus, nsidc-cumulus-prod-protected, nil, us-west-2, https://s3.us-west-2.amazonaws.com +gedil4a, ornl-cloud, s3, ornl-cumulus-prod-protected/gedi/GEDI_L4A_AGB_Density_V2_1/data, nil, us-west-2, https://s3.us-west-2.amazonaws.com gedil4b, ornl-cloud, s3, /vsis3/ornl-cumulus-prod-protected/gedi/GEDI_L4B_Gridded_Biomass_V2_1/data, GEDI04_B_MW019MW223_02_002_02_R01000M_V2.tif, us-west-2, https://s3.us-west-2.amazonaws.com gedil3-elevation, ornl-cloud, s3, /vsis3/ornl-cumulus-prod-protected/gedi/GEDI_L3_LandSurface_Metrics_V2/data, GEDI03_elev_lowestmode_mean_2019108_2022019_002_03.tif, us-west-2, https://s3.us-west-2.amazonaws.com gedil3-canopy, ornl-cloud, s3, /vsis3/ornl-cumulus-prod-protected/gedi/GEDI_L3_LandSurface_Metrics_V2/data, GEDI03_rh100_mean_2019108_2022019_002_03.tif, us-west-2, https://s3.us-west-2.amazonaws.com gedil3-elevation-stddev, ornl-cloud, s3, /vsis3/ornl-cumulus-prod-protected/gedi/GEDI_L3_LandSurface_Metrics_V2/data, GEDI03_elev_lowestmode_stddev_2019108_2022019_002_03.tif, us-west-2, https://s3.us-west-2.amazonaws.com gedil3-canopy-stddev, ornl-cloud, s3, /vsis3/ornl-cumulus-prod-protected/gedi/GEDI_L3_LandSurface_Metrics_V2/data, GEDI03_rh100_stddev_2019108_2022019_002_03.tif, us-west-2, https://s3.us-west-2.amazonaws.com gedil3-counts, ornl-cloud, s3, /vsis3/ornl-cumulus-prod-protected/gedi/GEDI_L3_LandSurface_Metrics_V2/data, GEDI03_counts_2019108_2022019_002_03.tif, us-west-2, https://s3.us-west-2.amazonaws.com -gedil2a, iam-role, s3, sliderule/data/GEDI, nil, us-west-2, https://s3.us-west-2.amazonaws.com -gedil1b, iam-role, s3, sliderule/data/GEDI, nil, us-west-2, https://s3.us-west-2.amazonaws.com -merit-dem, iam-role, s3, sliderule/data/MERIT, nil, us-west-2, https://s3.us-west-2.amazonaws.com -swot-sim-ecco-llc4320, podaac-cloud, s3, podaac-ops-cumulus-protected/SWOT_SIMULATED_L2_KARIN_SSH_ECCO_LLC4320_CALVAL_V1, nil, us-west-2, https://s3.us-west-2.amazonaws.com -swot-sim-glorys, podaac-cloud, s3, podaac-ops-cumulus-protected/SWOT_SIMULATED_L2_KARIN_SSH_GLORYS_CALVAL_V1, nil, us-west-2, https://s3.us-west-2.amazonaws.com -usgs3dep-1meter-dem, nil, nil, /vsis3/prd-tnm, nil, us-west-2, https://s3.us-west-2.amazonaws.com +gedil2a, iam-role, s3, sliderule/data/GEDI, nil, us-west-2, https://s3.us-west-2.amazonaws.com +gedil1b, iam-role, s3, sliderule/data/GEDI, nil, us-west-2, https://s3.us-west-2.amazonaws.com +merit-dem, iam-role, s3, sliderule/data/MERIT, nil, us-west-2, https://s3.us-west-2.amazonaws.com +swot-sim-ecco-llc4320, podaac-cloud, s3, podaac-ops-cumulus-protected/SWOT_SIMULATED_L2_KARIN_SSH_ECCO_LLC4320_CALVAL_V1, nil, us-west-2, https://s3.us-west-2.amazonaws.com +swot-sim-glorys, podaac-cloud, s3, podaac-ops-cumulus-protected/SWOT_SIMULATED_L2_KARIN_SSH_GLORYS_CALVAL_V1, nil, us-west-2, https://s3.us-west-2.amazonaws.com +usgs3dep-1meter-dem, nil, nil, /vsis3/prd-tnm, nil, us-west-2, https://s3.us-west-2.amazonaws.com esa-worldcover-10meter, nil, nil, /vsis3/esa-worldcover/v200/2021/map, /vsis3/sliderule/data/WORLDCOVER/ESA_WorldCover_10m_2021_v200_Map.vrt, eu-central-1, https://s3.eu-central-1.amazonaws.com -landsat-hls, lpdaac-cloud, nil, /vsis3/lp-prod-protected, nil, us-west-2, https://s3.us-west-2.amazonaws.com -arcticdem-mosaic, nil, nil, /vsis3/pgc-opendata-dems/arcticdem/mosaics/v4.1, 2m_dem_tiles.vrt, us-west-2, https://s3.us-west-2.amazonaws.com -arcticdem-strips, nil, nil, /vsis3/pgc-opendata-dems/arcticdem/strips/s2s041/2m, nil, us-west-2, https://s3.us-west-2.amazonaws.com -rema-mosaic, nil, nil, /vsis3/pgc-opendata-dems/rema/mosaics/v2.0/2m, 2m_dem_tiles.vrt, us-west-2, https://s3.us-west-2.amazonaws.com -rema-strips, nil, nil, /vsis3/pgc-opendata-dems/rema/strips/s2s041/2m, nil, us-west-2, https://s3.us-west-2.amazonaws.com -atlas-local, local, file, /data/ATLAS, nil, local, local -gedi-local, local, file, /data/GEDI, nil, local, local -swot-local, nil, file, /data/SWOT, nil, local, local -atlas-s3, iam-role, s3, sliderule/data/ATLAS, nil, us-west-2, https://s3.us-west-2.amazonaws.com -nsidc-s3, nsidc-cloud, cumulus, nsidc-cumulus-prod-protected, nil, us-west-2, https://s3.us-west-2.amazonaws.com -swot-s3, iam-role, s3, sliderule/data/SWOT, nil, us-west-2, https://s3.us-west-2.amazonaws.com \ No newline at end of file +landsat-hls, lpdaac-cloud, nil, /vsis3/lp-prod-protected, nil, us-west-2, https://s3.us-west-2.amazonaws.com +arcticdem-mosaic, nil, nil, /vsis3/pgc-opendata-dems/arcticdem/mosaics/v4.1, 2m_dem_tiles.vrt, us-west-2, https://s3.us-west-2.amazonaws.com +arcticdem-strips, nil, nil, /vsis3/pgc-opendata-dems/arcticdem/strips/s2s041/2m, nil, us-west-2, https://s3.us-west-2.amazonaws.com +rema-mosaic, nil, nil, /vsis3/pgc-opendata-dems/rema/mosaics/v2.0/2m, 2m_dem_tiles.vrt, us-west-2, https://s3.us-west-2.amazonaws.com +rema-strips, nil, nil, /vsis3/pgc-opendata-dems/rema/strips/s2s041/2m, nil, us-west-2, https://s3.us-west-2.amazonaws.com +atlas-local, local, file, /data/ATLAS, nil, local, local +gedi-local, local, file, /data/GEDI, nil, local, local +swot-local, nil, file, /data/SWOT, nil, local, local +atlas-s3, iam-role, s3, sliderule/data/ATLAS, nil, us-west-2, https://s3.us-west-2.amazonaws.com +nsidc-s3, nsidc-cloud, cumulus, nsidc-cumulus-prod-protected, nil, us-west-2, https://s3.us-west-2.amazonaws.com +swot-s3, iam-role, s3, sliderule/data/SWOT, nil, us-west-2, https://s3.us-west-2.amazonaws.com +sliderule-stage, iam-role, s3, sliderule-public, nil, us-west-2, https://s3.us-west-2.amazonaws.com \ No newline at end of file From 4e9e0c8ca1636e90fda267cab181d2f201484143 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 22 Feb 2024 20:33:55 +0000 Subject: [PATCH 15/43] updated pytests with new region column; alerts replaced generateException; parquet output to s3 can use a staging bucket --- clients/python/sliderule/gedi.py | 2 +- clients/python/sliderule/icesat2.py | 4 +- clients/python/sliderule/sliderule.py | 15 ++- clients/python/tests/test_arcticdem.py | 4 +- clients/python/tests/test_parquet.py | 6 +- packages/arrow/ArrowParms.cpp | 6 +- packages/arrow/ParquetBuilder.cpp | 64 +++++++++++-- packages/arrow/ParquetBuilder.h | 10 ++ packages/aws/CredentialStore.cpp | 8 +- packages/aws/CredentialStore.h | 4 +- packages/core/EventLib.cpp | 46 +++++++++- packages/core/EventLib.h | 13 ++- packages/core/LuaEndpoint.cpp | 31 ------- packages/core/LuaEndpoint.h | 16 ---- packages/core/LuaLibraryMsg.cpp | 2 +- packages/geo/RasterSampler.cpp | 6 +- packages/netsvc/EndpointProxy.cpp | 4 +- plugins/gedi/plugin/FootprintReader.h | 7 +- plugins/gedi/plugin/Gedi01bReader.cpp | 3 +- plugins/gedi/plugin/Gedi02aReader.cpp | 3 +- plugins/gedi/plugin/Gedi04aReader.cpp | 3 +- plugins/icesat2/plugin/Atl03Reader.cpp | 16 ++-- plugins/icesat2/plugin/Atl06Reader.cpp | 10 +- plugins/icesat2/plugin/Icesat2Parms.cpp | 116 ++++++++++++++++++++++++ plugins/icesat2/plugin/Icesat2Parms.h | 7 +- plugins/swot/plugin/SwotL2Reader.cpp | 6 +- scripts/apps/server.lua | 1 + 27 files changed, 293 insertions(+), 120 deletions(-) diff --git a/clients/python/sliderule/gedi.py b/clients/python/sliderule/gedi.py index ca5a1211d..8a6b5b954 100644 --- a/clients/python/sliderule/gedi.py +++ b/clients/python/sliderule/gedi.py @@ -58,7 +58,7 @@ def __flattenbatches(rsps, rectype, batch_column, parm, keep_id, as_numpy_array, # Check for Output Options if "output" in parm: - gdf = sliderule.procoutputfile(parm) + gdf = sliderule.procoutputfile(parm, rsps) profiles["flatten"] = time.perf_counter() - tstart_flatten return gdf diff --git a/clients/python/sliderule/icesat2.py b/clients/python/sliderule/icesat2.py index 36ced765f..6cdd78869 100644 --- a/clients/python/sliderule/icesat2.py +++ b/clients/python/sliderule/icesat2.py @@ -151,7 +151,7 @@ def __flattenbatches(rsps, rectype, batch_column, parm, keep_id, as_numpy_array, # Check for Output Options if "output" in parm: - gdf = sliderule.procoutputfile(parm) + gdf = sliderule.procoutputfile(parm, rsps) profiles["flatten"] = time.perf_counter() - tstart_flatten return gdf @@ -561,7 +561,7 @@ def atl03sp(parm, callbacks={}, resources=None, keep_id=False, height_key=None): # Check for Output Options if "output" in parm: profiles[atl03sp.__name__] = time.perf_counter() - tstart - return sliderule.procoutputfile(parm) + return sliderule.procoutputfile(parm, rsps) else: # Native Output # Flatten Responses tstart_flatten = time.perf_counter() diff --git a/clients/python/sliderule/sliderule.py b/clients/python/sliderule/sliderule.py index 8a67db27b..e0fcf0ca8 100644 --- a/clients/python/sliderule/sliderule.py +++ b/clients/python/sliderule/sliderule.py @@ -547,18 +547,25 @@ def emptyframe(**kwargs): # # Process Output File # -def procoutputfile(parm): +def procoutputfile(parm, rsps): output = parm["output"] + path = output["path"] + # Check If Remote Record Is In Responses + for rsp in rsps: + if 'arrowrec.remote' == rsp['__rectype']: + path = rsp['url'] + break + # Handle Local Files if "open_on_complete" in output and output["open_on_complete"]: if "as_geo" in output and not output["as_geo"]: # Return Parquet File as DataFrame - return geopandas.pd.read_parquet(output["path"]) + return geopandas.pd.read_parquet(path) else: # Return GeoParquet File as GeoDataFrame - return geopandas.read_parquet(output["path"]) + return geopandas.read_parquet(path) else: # Return Parquet Filename - return output["path"] + return path # # Get Values from Raw Buffer diff --git a/clients/python/tests/test_arcticdem.py b/clients/python/tests/test_arcticdem.py index f8ee7dcc0..77efbcaf9 100644 --- a/clients/python/tests/test_arcticdem.py +++ b/clients/python/tests/test_arcticdem.py @@ -61,7 +61,7 @@ def test_nearestneighbour(self, init): gdf = icesat2.atl06p(parms, resources=[resource]) assert init assert len(gdf) == 957 - assert len(gdf.keys()) == 19 + assert len(gdf.keys()) == 20 assert gdf["rgt"][0] == 1160 assert gdf["cycle"][0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 @@ -82,7 +82,7 @@ def test_zonal_stats(self, init): gdf = icesat2.atl06p(parms, resources=[resource]) assert init assert len(gdf) == 957 - assert len(gdf.keys()) == 26 + assert len(gdf.keys()) == 27 assert gdf["rgt"][0] == 1160 assert gdf["cycle"][0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 diff --git a/clients/python/tests/test_parquet.py b/clients/python/tests/test_parquet.py index e1b7ea67d..d2021e614 100644 --- a/clients/python/tests/test_parquet.py +++ b/clients/python/tests/test_parquet.py @@ -27,7 +27,7 @@ def test_atl06(self, init): os.remove("testfile1.parquet") assert init assert len(gdf) == 957 - assert len(gdf.keys()) == 16 + assert len(gdf.keys()) == 17 assert gdf["rgt"][0] == 1160 assert gdf["cycle"][0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 @@ -48,7 +48,7 @@ def test_atl06_non_geo(self, init): os.remove("testfile5.parquet") assert init assert len(gdf) == 957 - assert len(gdf.keys()) == 17 + assert len(gdf.keys()) == 18 assert gdf["rgt"][0] == 1160 assert gdf["cycle"][0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 @@ -69,7 +69,7 @@ def test_atl03(self, init): os.remove("testfile2.parquet") assert init assert len(gdf) == 190491 - assert len(gdf.keys()) == 23 + assert len(gdf.keys()) == 24 assert gdf["rgt"][0] == 1160 assert gdf["cycle"][0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 diff --git a/packages/arrow/ArrowParms.cpp b/packages/arrow/ArrowParms.cpp index 51f565f62..410f95fb1 100644 --- a/packages/arrow/ArrowParms.cpp +++ b/packages/arrow/ArrowParms.cpp @@ -36,7 +36,6 @@ #include "core.h" #include "ArrowParms.h" - /****************************************************************************** * STATIC DATA ******************************************************************************/ @@ -163,7 +162,10 @@ ArrowParms::ArrowParms (lua_State* L, int index): } else if(asset_name != NULL) { - credentials = CredentialStore::get(asset_name); + Asset* asset = dynamic_cast(LuaObject::getLuaObjectByName(asset_name, Asset::OBJECT_TYPE)); + const char* identity = asset->getIdentity(); + credentials = CredentialStore::get(identity); + asset->releaseLuaObject(); if(credentials.provided) { mlog(DEBUG, "Setting %s from asset %s", CREDENTIALS, asset_name); diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index 1963acb5e..7417c5dc0 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -74,6 +74,12 @@ const RecordObject::fieldDef_t ParquetBuilder::dataRecDef[] = { {"data", RecordObject::UINT8, offsetof(arrow_file_data_t, data), 0, NULL, NATIVE_FLAGS} // variable length }; +const char* ParquetBuilder::remoteRecType = "arrowrec.remote"; +const RecordObject::fieldDef_t ParquetBuilder::remoteRecDef[] = { + {"url", RecordObject::STRING, offsetof(arrow_file_remote_t, url), URL_MAX_LEN, NULL, NATIVE_FLAGS}, + {"size", RecordObject::INT64, offsetof(arrow_file_remote_t, size), 1, NULL, NATIVE_FLAGS} +}; + const char* ParquetBuilder::TMP_FILE_PREFIX = "/tmp/"; /****************************************************************************** @@ -422,6 +428,10 @@ struct ParquetBuilder::impl int ParquetBuilder::luaCreate (lua_State* L) { ArrowParms* _parms = NULL; + geo_data_t geo = { + .x_key = NULL, + .y_key = NULL + }; try { @@ -436,7 +446,6 @@ int ParquetBuilder::luaCreate (lua_State* L) const char* index_key = getLuaString(L, 8, true, NULL); /* Build Geometry Fields */ - geo_data_t geo; geo.as_geo = _parms->as_geo; if(geo.as_geo && (x_key != NULL) && (y_key != NULL)) { @@ -467,6 +476,8 @@ int ParquetBuilder::luaCreate (lua_State* L) catch(const RunTimeException& e) { if(_parms) _parms->releaseLuaObject(); + delete [] geo.x_key; + delete [] geo.y_key; mlog(e.level(), "Error creating %s: %s", LUA_META_NAME, e.what()); return returnLuaStatus(L, false); } @@ -479,6 +490,7 @@ void ParquetBuilder::init (void) { RECDEF(metaRecType, metaRecDef, sizeof(arrow_file_meta_t), NULL); RECDEF(dataRecType, dataRecDef, sizeof(arrow_file_data_t), NULL); + RECDEF(remoteRecType, remoteRecDef, sizeof(arrow_file_remote_t), NULL); } /*---------------------------------------------------------------------------- @@ -511,6 +523,33 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, assert(rec_type); assert(id); + /* Check Path */ + if((parms->path == NULL) || (parms->path[0] == '\0')) + { + if(parms->asset_name) + { + /* Generate Output Path */ + Asset* asset = dynamic_cast(LuaObject::getLuaObjectByName(parms->asset_name, Asset::OBJECT_TYPE)); + const char* path_prefix = StringLib::match(asset->getDriver(), "s3") ? "s3://" : ""; + const char* path_suffix = parms->as_geo ? ".geoparquet" : ".parquet"; + FString path_name("/%s.%016lX", id, OsApi::time(OsApi::CPU_CLK)); + FString path_str("%s%s%s%s", path_prefix, asset->getPath(), path_name.c_str(), path_suffix); + asset->releaseLuaObject(); + + /* Set Output Path */ + outputPath = path_str.c_str(true); + mlog(INFO, "Generating unique path: %s", outputPath); + } + else + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to determine output path"); + } + } + else + { + outputPath = StringLib::duplicate(parms->path); + } + /* Allocate Private Implementation */ pimpl = new ParquetBuilder::impl; @@ -586,6 +625,7 @@ ParquetBuilder::~ParquetBuilder(void) delete builderPid; parms->releaseLuaObject(); delete [] fileName; + delete [] outputPath; delete [] recType; delete outQ; delete inQ; @@ -689,10 +729,10 @@ void* ParquetBuilder::builderThread(void* parm) (void)builder->pimpl->parquetWriter->Close(); /* Send File to User */ - const char* _path = builder->parms->path; + const char* _path = builder->outputPath; uint32_t send_trace_id = start_trace(INFO, trace_id, "send_file", "{\"path\": \"%s\"}", _path); int _path_len = StringLib::size(_path); - if((_path_len > 5) && + if( (_path_len > 5) && (_path[0] == 's') && (_path[1] == '3') && (_path[2] == ':') && @@ -1229,7 +1269,7 @@ bool ParquetBuilder::send2S3 (const char* s3dst) if(status) { /* Send Initial Status */ - LuaEndpoint::generateExceptionStatus(RTE_INFO, INFO, outQ, NULL, "Initiated upload of results to S3, bucket = %s, key = %s", bucket, key); + alert(RTE_INFO, INFO, outQ, NULL, "Initiated upload of results to S3, bucket = %s, key = %s", bucket, key); try { @@ -1237,14 +1277,24 @@ bool ParquetBuilder::send2S3 (const char* s3dst) int64_t bytes_uploaded = S3CurlIODriver::put(fileName, bucket, key, parms->region, &parms->credentials); /* Send Successful Status */ - LuaEndpoint::generateExceptionStatus(RTE_INFO, INFO, outQ, NULL, "Upload to S3 completed, bucket = %s, key = %s, size = %ld", bucket, key, bytes_uploaded); + alert(RTE_INFO, INFO, outQ, NULL, "Upload to S3 completed, bucket = %s, key = %s, size = %ld", bucket, key, bytes_uploaded); + + /* Send Remote Record */ + RecordObject remote_record(remoteRecType); + arrow_file_remote_t* remote = (arrow_file_remote_t*)remote_record.getRecordData(); + StringLib::copy(&remote->url[0], outputPath, URL_MAX_LEN); + remote->size = bytes_uploaded; + if(!remote_record.post(outQ)) + { + mlog(CRITICAL, "Failed to send remote record back to user for %s", outputPath); + } } catch(const RunTimeException& e) { status = false; /* Send Error Status */ - LuaEndpoint::generateExceptionStatus(RTE_ERROR, e.level(), outQ, NULL, "Upload to S3 failed, bucket = %s, key = %s, error = %s", bucket, key, e.what()); + alert(RTE_ERROR, e.level(), outQ, NULL, "Upload to S3 failed, bucket = %s, key = %s, error = %s", bucket, key, e.what()); } } @@ -1255,7 +1305,7 @@ bool ParquetBuilder::send2S3 (const char* s3dst) return status; #else - LuaEndpoint::generateExceptionStatus(RTE_ERROR, CRITICAL, outQ, NULL, "Output path specifies S3, but server compiled without AWS support"); + alert(RTE_ERROR, CRITICAL, outQ, NULL, "Output path specifies S3, but server compiled without AWS support"); return false; #endif } diff --git a/packages/arrow/ParquetBuilder.h b/packages/arrow/ParquetBuilder.h index 29a5c72a2..f60db505e 100644 --- a/packages/arrow/ParquetBuilder.h +++ b/packages/arrow/ParquetBuilder.h @@ -66,6 +66,7 @@ class ParquetBuilder: public LuaObject static const int LIST_BLOCK_SIZE = 32; static const int FILE_NAME_MAX_LEN = 128; + static const int URL_MAX_LEN = 512; static const int FILE_BUFFER_RSPS_SIZE = 0x2000000; // 32MB static const int ROW_GROUP_SIZE = 0x4000000; // 64MB static const int QUEUE_BUFFER_FACTOR = 3; @@ -80,6 +81,9 @@ class ParquetBuilder: public LuaObject static const char* dataRecType; static const RecordObject::fieldDef_t dataRecDef[]; + static const char* remoteRecType; + static const RecordObject::fieldDef_t remoteRecDef[]; + static const char* TMP_FILE_PREFIX; /*-------------------------------------------------------------------- @@ -96,6 +100,11 @@ class ParquetBuilder: public LuaObject uint8_t data[FILE_BUFFER_RSPS_SIZE]; } arrow_file_data_t; + typedef struct { + char url[URL_MAX_LEN]; + long size; + } arrow_file_remote_t; + /*-------------------------------------------------------------------- * Methods *--------------------------------------------------------------------*/ @@ -152,6 +161,7 @@ class ParquetBuilder: public LuaObject int batchRowSizeBytes; int maxRowsInGroup; const char* fileName; // used locally to build file + const char* outputPath; // final destination of the file geo_data_t geoData; struct impl; // arrow implementation diff --git a/packages/aws/CredentialStore.cpp b/packages/aws/CredentialStore.cpp index cc67d18fa..cb3d1340c 100644 --- a/packages/aws/CredentialStore.cpp +++ b/packages/aws/CredentialStore.cpp @@ -84,7 +84,7 @@ void CredentialStore::deinit (void) /*---------------------------------------------------------------------------- * get *----------------------------------------------------------------------------*/ -CredentialStore::Credential CredentialStore::get (const char* host) +CredentialStore::Credential CredentialStore::get (const char* identity) { Credential credential; @@ -92,7 +92,7 @@ CredentialStore::Credential CredentialStore::get (const char* host) { try { - credential = credentialStore[host]; + credential = credentialStore[identity]; } catch(const RunTimeException& e) { @@ -107,14 +107,14 @@ CredentialStore::Credential CredentialStore::get (const char* host) /*---------------------------------------------------------------------------- * put *----------------------------------------------------------------------------*/ -bool CredentialStore::put (const char* host, const Credential& credential) +bool CredentialStore::put (const char* identity, const Credential& credential) { bool status = false; credentialLock.lock(); { /* Store Credentials */ - status = credentialStore.add(host, credential); + status = credentialStore.add(identity, credential); } credentialLock.unlock(); diff --git a/packages/aws/CredentialStore.h b/packages/aws/CredentialStore.h index d3b194423..e318127e7 100644 --- a/packages/aws/CredentialStore.h +++ b/packages/aws/CredentialStore.h @@ -240,8 +240,8 @@ class CredentialStore static void init (void); static void deinit (void); - static Credential get (const char* host); - static bool put (const char* host, const Credential& credential); + static Credential get (const char* identity); + static bool put (const char* identity, const Credential& credential); static int luaGet (lua_State* L); static int luaPut (lua_State* L); diff --git a/packages/core/EventLib.cpp b/packages/core/EventLib.cpp index 2074fa287..18673da3c 100644 --- a/packages/core/EventLib.cpp +++ b/packages/core/EventLib.cpp @@ -56,7 +56,7 @@ */ static Publisher* outq; -static RecordObject::fieldDef_t rec_def[] = +static RecordObject::fieldDef_t eventRecDef[] = { {"time", RecordObject::INT64, offsetof(EventLib::event_t, systime), 1, NULL, NATIVE_FLAGS}, {"tid", RecordObject::INT64, offsetof(EventLib::event_t, tid), 1, NULL, NATIVE_FLAGS}, @@ -70,11 +70,19 @@ static RecordObject::fieldDef_t rec_def[] = {"attr", RecordObject::STRING, offsetof(EventLib::event_t, attr), 0, NULL, NATIVE_FLAGS} }; +static RecordObject::fieldDef_t alertRecDef[] = +{ + {"code", RecordObject::INT32, offsetof(EventLib::alert_t, code), 1, NULL, NATIVE_FLAGS}, + {"level", RecordObject::INT32, offsetof(EventLib::alert_t, level), 1, NULL, NATIVE_FLAGS}, + {"text", RecordObject::STRING, offsetof(EventLib::alert_t, text), EventLib::MAX_ALERT_SIZE, NULL, NATIVE_FLAGS} +}; + /****************************************************************************** * STATIC DATA ******************************************************************************/ -const char* EventLib::rec_type = "eventrec"; +const char* EventLib::eventRecType = "eventrec"; +const char* EventLib::alertRecType = "exceptrec"; std::atomic EventLib::trace_id{1}; Thread::key_t EventLib::trace_key; @@ -92,8 +100,9 @@ event_level_t EventLib::metric_level; *----------------------------------------------------------------------------*/ void EventLib::init (const char* eventq) { - /* Define Event Record */ - RECDEF(rec_type, rec_def, offsetof(event_t, attr) + 1, NULL); + /* Define Records */ + RECDEF(eventRecType, eventRecDef, offsetof(event_t, attr) + 1, NULL); + RECDEF(alertRecType, alertRecDef, sizeof(alert_t), "code"); /* Create Thread Global */ trace_key = Thread::createGlobal(); @@ -329,6 +338,33 @@ void EventLib::logMsg(const char* file_name, unsigned int line_number, event_lev sendEvent(&event, attr_size); } +/*---------------------------------------------------------------------------- + * alertMsg + *----------------------------------------------------------------------------*/ +void EventLib::alertMsg (int code, event_level_t level, void* rspsq, bool* active, const char* errmsg, ...) +{ + /* Allocate and Initialize Alert Record */ + RecordObject record(alertRecType); + alert_t* alert = reinterpret_cast(record.getRecordData()); + alert->code = code; + alert->level = (int32_t)level; + + /* Build Message */ + va_list args; + va_start(args, errmsg); + int vlen = vsnprintf(alert->text, MAX_ALERT_SIZE - 1, errmsg, args); + int attr_size = MAX(MIN(vlen + 1, MAX_ALERT_SIZE), 1); + alert->text[attr_size - 1] = '\0'; + va_end(args); + + /* Generate Corresponding Log Message */ + mlog(level, "%s", alert->text); + + /* Post Alert Record */ + Publisher* rspsq_ptr = reinterpret_cast(rspsq); // avoids cyclic dependency with RecordObject + record.post(rspsq_ptr, 0, active); +} + /*---------------------------------------------------------------------------- * generateMetric *----------------------------------------------------------------------------*/ @@ -366,7 +402,7 @@ void EventLib::generateMetric (event_level_t lvl, const char* name, metric_subty int EventLib::sendEvent (event_t* event, int attr_size) { int event_record_size = offsetof(event_t, attr) + attr_size; - RecordObject record(rec_type, event_record_size, false); + RecordObject record(eventRecType, event_record_size, false); event_t* data = (event_t*)record.getRecordData(); memcpy(data, event, event_record_size); return record.post(outq, 0, NULL, false); diff --git a/packages/core/EventLib.h b/packages/core/EventLib.h index 09fde2fd9..3affb96c0 100644 --- a/packages/core/EventLib.h +++ b/packages/core/EventLib.h @@ -47,6 +47,7 @@ ******************************************************************************/ #define mlog(lvl,...) EventLib::logMsg(__FILE__,__LINE__,lvl,__VA_ARGS__) +#define alert(code,lvl,outq,active,...) EventLib::alertMsg(code,lvl,outq,active,__VA_ARGS__) #ifdef __tracing__ #define start_trace(lvl, parent, name, fmt, ...) EventLib::startTrace(parent, name, lvl, fmt, __VA_ARGS__) @@ -75,8 +76,10 @@ class EventLib static const int MAX_ATTR_SIZE = 1024; static const int MAX_METRICS = 128; static const int32_t INVALID_METRIC = -1; + static const int MAX_ALERT_SIZE = 256; - static const char* rec_type; + static const char* alertRecType; + static const char* eventRecType; /*-------------------------------------------------------------------- * Types @@ -111,6 +114,12 @@ class EventLib GAUGE = 1 } metric_subtype_t; + typedef struct { + int32_t code; + int32_t level; + char text[MAX_ALERT_SIZE]; + } alert_t; + /*-------------------------------------------------------------------- * Methods *--------------------------------------------------------------------*/ @@ -131,7 +140,7 @@ class EventLib static uint32_t grabId (void); static void logMsg (const char* file_name, unsigned int line_number, event_level_t lvl, const char* msg_fmt, ...) VARG_CHECK(printf, 4, 5); - + static void alertMsg (int code, event_level_t level, void* rspsq, bool* active, const char* errmsg, ...) VARG_CHECK(printf, 5, 6); static void generateMetric (event_level_t lvl, const char* name, metric_subtype_t subtype, double value); private: diff --git a/packages/core/LuaEndpoint.cpp b/packages/core/LuaEndpoint.cpp index c605a8e1a..75a624f1a 100644 --- a/packages/core/LuaEndpoint.cpp +++ b/packages/core/LuaEndpoint.cpp @@ -46,13 +46,6 @@ const struct luaL_Reg LuaEndpoint::LUA_META_TABLE[] = { {NULL, NULL} }; -const char* LuaEndpoint::EndpointExceptionRecType = "exceptrec"; -const RecordObject::fieldDef_t LuaEndpoint::EndpointExceptionRecDef[] = { - {"code", RecordObject::INT32, offsetof(response_exception_t, code), 1, NULL, NATIVE_FLAGS}, - {"level", RecordObject::INT32, offsetof(response_exception_t, level), 1, NULL, NATIVE_FLAGS}, - {"text", RecordObject::STRING, offsetof(response_exception_t, text), MAX_EXCEPTION_TEXT_SIZE, NULL, NATIVE_FLAGS} -}; - const double LuaEndpoint::DEFAULT_NORMAL_REQUEST_MEMORY_THRESHOLD = 1.0; const double LuaEndpoint::DEFAULT_STREAM_REQUEST_MEMORY_THRESHOLD = 1.0; @@ -96,7 +89,6 @@ LuaEndpoint::Authenticator::~Authenticator(void) *----------------------------------------------------------------------------*/ void LuaEndpoint::init (void) { - RECDEF(EndpointExceptionRecType, EndpointExceptionRecDef, sizeof(response_exception_t), "code"); } /*---------------------------------------------------------------------------- @@ -121,29 +113,6 @@ int LuaEndpoint::luaCreate (lua_State* L) } } -/*---------------------------------------------------------------------------- - * generateExceptionStatus - *----------------------------------------------------------------------------*/ -void LuaEndpoint::generateExceptionStatus (int code, event_level_t level, Publisher* outq, bool* active, const char* errmsg, ...) -{ - /* Build Error Message */ - char error_buf[MAX_EXCEPTION_TEXT_SIZE]; - va_list args; - va_start(args, errmsg); - int vlen = vsnprintf(error_buf, MAX_EXCEPTION_TEXT_SIZE - 1, errmsg, args); - int attr_size = MAX(MIN(vlen + 1, MAX_EXCEPTION_TEXT_SIZE), 1); - error_buf[attr_size - 1] = '\0'; - va_end(args); - - /* Post Endpoint Exception Record */ - RecordObject record(EndpointExceptionRecType); - response_exception_t* exception = (response_exception_t*)record.getRecordData(); - exception->code = code; - exception->level = (int32_t)level; - StringLib::format(exception->text, MAX_EXCEPTION_TEXT_SIZE, "%s", error_buf); - record.post(outq, 0, active); -} - /****************************************************************************** * PROTECTED METHODS ******************************************************************************/ diff --git a/packages/core/LuaEndpoint.h b/packages/core/LuaEndpoint.h index d3095092e..54145bde3 100644 --- a/packages/core/LuaEndpoint.h +++ b/packages/core/LuaEndpoint.h @@ -59,28 +59,13 @@ class LuaEndpoint: public EndpointObject static const char* LUA_META_NAME; static const struct luaL_Reg LUA_META_TABLE[]; - static const char* EndpointExceptionRecType; - static const RecordObject::fieldDef_t EndpointExceptionRecDef[]; - static const double DEFAULT_NORMAL_REQUEST_MEMORY_THRESHOLD; static const double DEFAULT_STREAM_REQUEST_MEMORY_THRESHOLD; static const int MAX_RESPONSE_TIME_MS = 5000; - static const int MAX_EXCEPTION_TEXT_SIZE = 256; static const char* LUA_RESPONSE_QUEUE; static const char* LUA_REQUEST_ID; - /*-------------------------------------------------------------------- - * Typedefs - *--------------------------------------------------------------------*/ - - /* Response Exception Record */ - typedef struct { - int32_t code; - int32_t level; - char text[MAX_EXCEPTION_TEXT_SIZE]; - } response_exception_t; - /*-------------------------------------------------------------------- * Authenticator Subclass *--------------------------------------------------------------------*/ @@ -110,7 +95,6 @@ class LuaEndpoint: public EndpointObject static void init (void); static int luaCreate (lua_State* L); - static void generateExceptionStatus (int code, event_level_t level, Publisher* outq, bool* active, const char* errmsg, ...) VARG_CHECK(printf, 5, 6); protected: diff --git a/packages/core/LuaLibraryMsg.cpp b/packages/core/LuaLibraryMsg.cpp index d2cc4f786..081af5b03 100644 --- a/packages/core/LuaLibraryMsg.cpp +++ b/packages/core/LuaLibraryMsg.cpp @@ -490,7 +490,7 @@ int LuaLibraryMsg::lmsg_sendlog (lua_State* L) /* Post Record */ int rec_size = offsetof(EventLib::event_t, attr) + attr_size + 1; - RecordObject record(EventLib::rec_type, rec_size); + RecordObject record(EventLib::eventRecType, rec_size); memcpy(record.getRecordData(), &event, rec_size); uint8_t* rec_buf = NULL; int rec_bytes = record.serialize(&rec_buf, RecordObject::REFERENCE); diff --git a/packages/geo/RasterSampler.cpp b/packages/geo/RasterSampler.cpp index 91778b8f1..7f731b262 100644 --- a/packages/geo/RasterSampler.cpp +++ b/packages/geo/RasterSampler.cpp @@ -320,9 +320,9 @@ bool RasterSampler::processRecord (RecordObject* record, okey_t key, recVec_t* r /* Generate Error Messages */ if(err & SS_THREADS_LIMIT_ERROR) { - LuaEndpoint::generateExceptionStatus(RTE_ERROR, CRITICAL, outQ, NULL, - "Too many rasters to sample %s at %.3lf,%.3lf,%3lf: max allowed: %d, limit your AOI/temporal range or use filters", - rasterKey, lon_val, lat_val, height_val, GeoIndexedRaster::MAX_READER_THREADS); + alert(RTE_ERROR, CRITICAL, outQ, NULL, + "Too many rasters to sample %s at %.3lf,%.3lf,%3lf: max allowed: %d, limit your AOI/temporal range or use filters", + rasterKey, lon_val, lat_val, height_val, GeoIndexedRaster::MAX_READER_THREADS); } if(raster->hasZonalStats()) diff --git a/packages/netsvc/EndpointProxy.cpp b/packages/netsvc/EndpointProxy.cpp index bea145d62..4035117a8 100644 --- a/packages/netsvc/EndpointProxy.cpp +++ b/packages/netsvc/EndpointProxy.cpp @@ -250,7 +250,7 @@ void* EndpointProxy::collatorThread (void* parm) status = proxy->rqstPub->postCopy(¤t_resource, sizeof(current_resource), SYS_TIMEOUT); if(status < 0) { - LuaEndpoint::generateExceptionStatus(RTE_ERROR, ERROR, proxy->outQ, NULL, "Failed (%d) to post request for %s", status, proxy->resources[current_resource]); + alert(RTE_ERROR, ERROR, proxy->outQ, NULL, "Failed (%d) to post request for %s", status, proxy->resources[current_resource]); break; } } @@ -350,7 +350,7 @@ void* EndpointProxy::proxyThread (void* parm) /* Post Status */ int code = valid ? RTE_INFO : RTE_ERROR; event_level_t level = valid ? INFO : ERROR; - LuaEndpoint::generateExceptionStatus(code, level, proxy->outQ, NULL, "%s processing resource [%d out of %d]: %s", + alert(code, level, proxy->outQ, NULL, "%s processing resource [%d out of %d]: %s", valid ? "Successfully completed" : "Failed to complete", current_resource + 1, proxy->numResources, resource); } diff --git a/plugins/gedi/plugin/FootprintReader.h b/plugins/gedi/plugin/FootprintReader.h index cc4eee53e..d15542fde 100644 --- a/plugins/gedi/plugin/FootprintReader.h +++ b/plugins/gedi/plugin/FootprintReader.h @@ -262,12 +262,9 @@ FootprintReader::FootprintReader ( lua_State* L, Asset* _asset, con } catch(const RunTimeException& e) { - /* Log Error */ - mlog(e.level(), "Failed to process resource %s: %s", resource, e.what()); - /* Generate Exception Record */ - if(e.code() == RTE_TIMEOUT) LuaEndpoint::generateExceptionStatus(RTE_TIMEOUT, e.level(), outQ, &active, "%s: (%s)", e.what(), resource); - else LuaEndpoint::generateExceptionStatus(RTE_RESOURCE_DOES_NOT_EXIST, e.level(), outQ, &active, "%s: (%s)", e.what(), resource); + if(e.code() == RTE_TIMEOUT) alert(RTE_TIMEOUT, e.level(), outQ, &active, "%s: (%s)", e.what(), resource); + else alert(RTE_RESOURCE_DOES_NOT_EXIST, e.level(), outQ, &active, "%s: (%s)", e.what(), resource); /* Indicate End of Data */ if(sendTerminator) outQ->postCopy("", 0); diff --git a/plugins/gedi/plugin/Gedi01bReader.cpp b/plugins/gedi/plugin/Gedi01bReader.cpp index 16a64328d..70512eaa7 100644 --- a/plugins/gedi/plugin/Gedi01bReader.cpp +++ b/plugins/gedi/plugin/Gedi01bReader.cpp @@ -268,8 +268,7 @@ void* Gedi01bReader::subsettingThread (void* parm) } catch(const RunTimeException& e) { - mlog(e.level(), "Failure during processing of resource %s beam %d: %s", reader->resource, info->beam, e.what()); - LuaEndpoint::generateExceptionStatus(e.code(), e.level(), reader->outQ, &reader->active, "%s: (%s)", e.what(), reader->resource); + alert(e.code(), e.level(), reader->outQ, &reader->active, "Failure on resource %s beam %d: %s", reader->resource, info->beam, e.what()); } /* Handle Global Reader Updates */ diff --git a/plugins/gedi/plugin/Gedi02aReader.cpp b/plugins/gedi/plugin/Gedi02aReader.cpp index e6a9425af..5e9a4e794 100644 --- a/plugins/gedi/plugin/Gedi02aReader.cpp +++ b/plugins/gedi/plugin/Gedi02aReader.cpp @@ -257,8 +257,7 @@ void* Gedi02aReader::subsettingThread (void* parm) } catch(const RunTimeException& e) { - mlog(e.level(), "Failure during processing of resource %s beam %d: %s", reader->resource, info->beam, e.what()); - LuaEndpoint::generateExceptionStatus(e.code(), e.level(), reader->outQ, &reader->active, "%s: (%s)", e.what(), reader->resource); + alert(e.code(), e.level(), reader->outQ, &reader->active, "Failure on resource %s beam %d: %s", reader->resource, info->beam, e.what()); } /* Handle Global Reader Updates */ diff --git a/plugins/gedi/plugin/Gedi04aReader.cpp b/plugins/gedi/plugin/Gedi04aReader.cpp index 78270f5ae..73915ea34 100644 --- a/plugins/gedi/plugin/Gedi04aReader.cpp +++ b/plugins/gedi/plugin/Gedi04aReader.cpp @@ -271,8 +271,7 @@ void* Gedi04aReader::subsettingThread (void* parm) } catch(const RunTimeException& e) { - mlog(e.level(), "Failure during processing of resource %s beam %d: %s", reader->resource, info->beam, e.what()); - LuaEndpoint::generateExceptionStatus(e.code(), e.level(), reader->outQ, &reader->active, "%s: (%s)", e.what(), reader->resource); + alert(e.code(), e.level(), reader->outQ, &reader->active, "Failure on resource %s beam %d: %s", reader->resource, info->beam, e.what()); } /* Handle Global Reader Updates */ diff --git a/plugins/icesat2/plugin/Atl03Reader.cpp b/plugins/icesat2/plugin/Atl03Reader.cpp index a0ef0d6a3..a68dad835 100644 --- a/plugins/icesat2/plugin/Atl03Reader.cpp +++ b/plugins/icesat2/plugin/Atl03Reader.cpp @@ -184,7 +184,8 @@ Atl03Reader::Atl03Reader (lua_State* L, Asset* _asset, const char* _resource, co { for(int pair = 0; pair < Icesat2Parms::NUM_PAIR_TRACKS; pair++) { - if(parms->track == Icesat2Parms::ALL_TRACKS || track == parms->track) + int gt_index = (2 * (track - 1)) + pair; + if(parms->beams[gt_index] && (parms->track == Icesat2Parms::ALL_TRACKS || track == parms->track)) { info_t* info = new info_t; info->reader = this; @@ -204,12 +205,9 @@ Atl03Reader::Atl03Reader (lua_State* L, Asset* _asset, const char* _resource, co } catch(const RunTimeException& e) { - /* Log Error */ - mlog(e.level(), "Failed to read global information in resource %s: %s", resource, e.what()); - /* Generate Exception Record */ - if(e.code() == RTE_TIMEOUT) LuaEndpoint::generateExceptionStatus(RTE_TIMEOUT, e.level(), outQ, &active, "%s: (%s)", e.what(), resource); - else LuaEndpoint::generateExceptionStatus(RTE_RESOURCE_DOES_NOT_EXIST, e.level(), outQ, &active, "%s: (%s)", e.what(), resource); + if(e.code() == RTE_TIMEOUT) alert(RTE_TIMEOUT, e.level(), outQ, &active, "Failure on resource %s: %s", resource, e.what()); + else alert(RTE_RESOURCE_DOES_NOT_EXIST, e.level(), outQ, &active, "Failure on resource %s: %s", resource, e.what()); /* Indicate End of Data */ if(sendTerminator) outQ->postCopy("", 0); @@ -1495,8 +1493,7 @@ void* Atl03Reader::subsettingThread (void* parm) } catch(const RunTimeException& e) { - mlog(e.level(), "Error posting results for resource %s track %d: %s", info->reader->resource, info->track, e.what()); - LuaEndpoint::generateExceptionStatus(e.code(), e.level(), reader->outQ, &reader->active, "%s: (%s)", e.what(), info->reader->resource); + alert(e.code(), e.level(), reader->outQ, &reader->active, "Error posting results for resource %s track %d: %s", info->reader->resource, info->track, e.what()); } /* Clean Up Records */ @@ -1516,8 +1513,7 @@ void* Atl03Reader::subsettingThread (void* parm) } catch(const RunTimeException& e) { - mlog(e.level(), "Failure during processing of resource %s track %d: %s", info->reader->resource, info->track, e.what()); - LuaEndpoint::generateExceptionStatus(e.code(), e.level(), reader->outQ, &reader->active, "%s: (%s)", e.what(), info->reader->resource); + alert(e.code(), e.level(), reader->outQ, &reader->active, "Failure on resource %s track %d: %s", info->reader->resource, info->track, e.what()); } /* Handle Global Reader Updates */ diff --git a/plugins/icesat2/plugin/Atl06Reader.cpp b/plugins/icesat2/plugin/Atl06Reader.cpp index 2f8d323dd..ab88ff996 100644 --- a/plugins/icesat2/plugin/Atl06Reader.cpp +++ b/plugins/icesat2/plugin/Atl06Reader.cpp @@ -205,12 +205,9 @@ Atl06Reader::Atl06Reader (lua_State* L, Asset* _asset, const char* _resource, co } catch(const RunTimeException& e) { - /* Log Error */ - mlog(e.level(), "Failed to read global information in resource %s: %s", resource, e.what()); - /* Generate Exception Record */ - if(e.code() == RTE_TIMEOUT) LuaEndpoint::generateExceptionStatus(RTE_TIMEOUT, e.level(), outQ, &active, "%s: (%s)", e.what(), resource); - else LuaEndpoint::generateExceptionStatus(RTE_RESOURCE_DOES_NOT_EXIST, e.level(), outQ, &active, "%s: (%s)", e.what(), resource); + if(e.code() == RTE_TIMEOUT) alert(RTE_TIMEOUT, e.level(), outQ, &active, "Failure on resource %s: %s", resource, e.what()); + else alert(RTE_RESOURCE_DOES_NOT_EXIST, e.level(), outQ, &active, "Failure on resource %s: %s", resource, e.what()); /* Indicate End of Data */ if(sendTerminator) outQ->postCopy("", 0); @@ -639,8 +636,7 @@ void* Atl06Reader::subsettingThread (void* parm) } catch(const RunTimeException& e) { - mlog(e.level(), "Failure during processing of resource %s track %d: %s", info->reader->resource, info->track, e.what()); - LuaEndpoint::generateExceptionStatus(e.code(), e.level(), reader->outQ, &reader->active, "%s: (%s)", e.what(), info->reader->resource); + alert(e.code(), e.level(), reader->outQ, &reader->active, "Failure on resource %s track %d: %s", info->reader->resource, info->track, e.what()); } /* Handle Global Reader Updates */ diff --git a/plugins/icesat2/plugin/Icesat2Parms.cpp b/plugins/icesat2/plugin/Icesat2Parms.cpp index 785ae13de..bc7731466 100644 --- a/plugins/icesat2/plugin/Icesat2Parms.cpp +++ b/plugins/icesat2/plugin/Icesat2Parms.cpp @@ -243,6 +243,20 @@ Icesat2Parms::phoreal_geoloc_t Icesat2Parms::str2geoloc (const char* fmt_str) return PHOREAL_UNSUPPORTED; } +/*---------------------------------------------------------------------------- + * str2geoloc + *----------------------------------------------------------------------------*/ +Icesat2Parms::gt_t Icesat2Parms::str2gt (const char* gt_str) +{ + if(StringLib::match(gt_str, "gt1l")) return GT1L; + if(StringLib::match(gt_str, "gt1r")) return GT1R; + if(StringLib::match(gt_str, "gt2l")) return GT2L; + if(StringLib::match(gt_str, "gt2r")) return GT2R; + if(StringLib::match(gt_str, "gt3l")) return GT3L; + if(StringLib::match(gt_str, "gt3r")) return GT3R; + return INVALID_GT; +} + /****************************************************************************** * PRIVATE METHODS ******************************************************************************/ @@ -258,6 +272,7 @@ Icesat2Parms::Icesat2Parms(lua_State* L, int index): atl03_cnf { false, false, true, true, true, true, true }, quality_ph { true, false, false, false }, atl08_class { false, false, false, false, false }, + beams { true, true, true, true, true, true}, stages { true, false, false, false }, yapc { .score = 0, .version = 3, @@ -770,6 +785,107 @@ void Icesat2Parms::get_lua_atl08_class (lua_State* L, int index, bool* provided) } } +/*---------------------------------------------------------------------------- + * get_lua_beams + *----------------------------------------------------------------------------*/ +void Icesat2Parms::get_lua_beams (lua_State* L, int index, bool* provided) +{ + /* Reset Provided */ + if(provided) *provided = false; + + /* Must be table of classifications or a single classification as a string */ + if(lua_istable(L, index)) + { + /* Clear classification table (sets all to false) */ + memset(beams, 0, sizeof(beams)); + if(provided) *provided = true; + + /* Iterate through each beam in table */ + int num_beams = lua_rawlen(L, index); + for(int i = 0; i < num_beams; i++) + { + /* Get beam */ + lua_rawgeti(L, index, i+1); + + /* Set beam */ + if(lua_isinteger(L, -1)) + { + int beam = LuaObject::getLuaInteger(L, -1); + switch(beam) + { + case GT1L: beams[gt2index(GT1L)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + case GT1R: beams[gt2index(GT1R)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + case GT2L: beams[gt2index(GT2L)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + case GT2R: beams[gt2index(GT2R)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + case GT3L: beams[gt2index(GT3L)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + case GT3R: beams[gt2index(GT3R)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + default: mlog(ERROR, "Invalid beam: %d", beam); break; + + } + } + else if(lua_isstring(L, -1)) + { + const char* beam_str = LuaObject::getLuaString(L, -1); + gt_t gt = str2gt(beam_str); + int gt_index = gt2index(static_cast(gt)); + if(gt_index != INVALID_GT) + { + beams[gt_index] = true; + mlog(DEBUG, "Selecting beam %s", beam_str); + } + else + { + mlog(ERROR, "Invalid beam: %s", beam_str); + } + } + + /* Clean up stack */ + lua_pop(L, 1); + } + } + else if(lua_isinteger(L, index)) + { + /* Clear beam table (sets all to false) */ + memset(beams, 0, sizeof(beams)); + + /* Set beam */ + int beam = LuaObject::getLuaInteger(L, -1); + switch(beam) + { + case GT1L: beams[gt2index(GT1L)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + case GT1R: beams[gt2index(GT1R)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + case GT2L: beams[gt2index(GT2L)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + case GT2R: beams[gt2index(GT2R)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + case GT3L: beams[gt2index(GT3L)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + case GT3R: beams[gt2index(GT3R)] = true; mlog(DEBUG, "Selecting beam %d", beam); break; + default: mlog(ERROR, "Invalid beam: %d", beam); break; + } + } + else if(lua_isstring(L, index)) + { + /* Clear beam table (sets all to false) */ + memset(beams, 0, sizeof(beams)); + + /* Set beam */ + const char* beam_str = LuaObject::getLuaString(L, -1); + gt_t gt = str2gt(beam_str); + int gt_index = gt2index(static_cast(gt)); + if(gt_index != INVALID_GT) + { + beams[gt_index] = true; + mlog(DEBUG, "Selecting beam %s", beam_str); + } + else + { + mlog(ERROR, "Invalid beam: %s", beam_str); + } + } + else if(!lua_isnil(L, index)) + { + mlog(ERROR, "Beam selection must be provided as a table or string"); + } +} + /*---------------------------------------------------------------------------- * get_lua_yapc *----------------------------------------------------------------------------*/ diff --git a/plugins/icesat2/plugin/Icesat2Parms.h b/plugins/icesat2/plugin/Icesat2Parms.h index 3db2fa390..95f8db3e0 100644 --- a/plugins/icesat2/plugin/Icesat2Parms.h +++ b/plugins/icesat2/plugin/Icesat2Parms.h @@ -124,7 +124,8 @@ class Icesat2Parms: public NetsvcParms GT2L = 30, GT2R = 40, GT3L = 50, - GT3R = 60 + GT3R = 60, + INVALID_GT = 70 } gt_t; /* Spots */ @@ -236,6 +237,8 @@ class Icesat2Parms: public NetsvcParms static quality_ph_t str2atl03quality (const char* quality_ph_str); static atl08_classification_t str2atl08class (const char* classifiction_str); static phoreal_geoloc_t str2geoloc (const char* fmt_str); + static gt_t str2gt (const char* gt_str); + static int gt2index (int gt) { return (gt / 10) - 1; } /*-------------------------------------------------------------------- * Inline Methods @@ -270,6 +273,7 @@ class Icesat2Parms: public NetsvcParms bool atl03_cnf[NUM_SIGNAL_CONF]; // list of desired signal confidences of photons from atl03 classification bool quality_ph[NUM_PHOTON_QUALITY]; // list of desired photon quality levels from atl03 bool atl08_class[NUM_ATL08_CLASSES]; // list of surface classifications to use (leave empty to skip) + bool beams[NUM_SPOTS]; // list of which beams (gt[l|r][1|2|3]) bool stages[NUM_STAGES]; // algorithm iterations yapc_t yapc; // settings used in YAPC algorithm int track; // reference pair track number (1, 2, 3, or 0 for all tracks) @@ -299,6 +303,7 @@ class Icesat2Parms: public NetsvcParms void get_lua_atl03_cnf (lua_State* L, int index, bool* provided); void get_lua_atl03_quality (lua_State* L, int index, bool* provided); void get_lua_atl08_class (lua_State* L, int index, bool* provided); + void get_lua_beams (lua_State* L, int index, bool* provided); void get_lua_yapc (lua_State* L, int index, bool* provided); static void get_lua_field_list (lua_State* L, int index, AncillaryFields::list_t** string_list, bool* provided); void get_lua_phoreal (lua_State* L, int index, bool* provided); diff --git a/plugins/swot/plugin/SwotL2Reader.cpp b/plugins/swot/plugin/SwotL2Reader.cpp index 835f30ffa..d451d7b82 100644 --- a/plugins/swot/plugin/SwotL2Reader.cpp +++ b/plugins/swot/plugin/SwotL2Reader.cpp @@ -178,8 +178,7 @@ SwotL2Reader::SwotL2Reader (lua_State* L, Asset* _asset, const char* _resource, else { /* Report Empty Region */ - mlog(INFO, "Empty spatial region for %s", resource); - LuaEndpoint::generateExceptionStatus(RTE_INFO, INFO, outQ, &active, "Empty spatial region for %s", resource); + alert(RTE_INFO, INFO, outQ, &active, "Empty spatial region for %s", resource); /* Terminate */ threadCount = 0; @@ -480,8 +479,7 @@ void* SwotL2Reader::varThread (void* parm) } catch(const RunTimeException& e) { - mlog(e.level(), "Failure during processing of %s/%s: %s", reader->resource, info->variable_name, e.what()); - LuaEndpoint::generateExceptionStatus(e.code(), e.level(), reader->outQ, &reader->active, "%s: (%s)", e.what(), reader->resource); + alert(e.code(), e.level(), reader->outQ, &reader->active, "Failure on %s/%s: %s", reader->resource, info->variable_name, e.what()); } /* Update Statistics */ diff --git a/scripts/apps/server.lua b/scripts/apps/server.lua index 868527b43..b1497435b 100644 --- a/scripts/apps/server.lua +++ b/scripts/apps/server.lua @@ -65,6 +65,7 @@ local assets = asset.loaddir(asset_directory) -- Run IAM Role Authentication Script (identity="iam-role") -- local role_auth_script = core.script("iam_role_auth"):name("RoleAuthScript") +sys.wait(5) -- best effort delay to give time for iam role to be established -- Run Earth Data Authentication Scripts -- if authenticate_to_nsidc then From bbedfd4f99d51a57643ad1a2ac08094e87889e3d Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 22 Feb 2024 21:10:02 +0000 Subject: [PATCH 16/43] added public cluster protection for staging s3 files --- packages/arrow/ParquetBuilder.cpp | 33 +++++++++++-------- packages/core/LuaLibrarySys.cpp | 24 ++++++++++++++ packages/core/LuaLibrarySys.h | 1 + platforms/linux/OsApi.cpp | 17 ++++++++++ platforms/linux/OsApi.h | 3 ++ scripts/apps/server.lua | 4 +++ targets/slideruleearth-aws/Makefile | 2 +- targets/slideruleearth-aws/docker-compose.yml | 1 + .../cluster/docker-compose-sliderule.yml | 1 + .../terraform/cluster/sliderule-asg.tf | 3 +- 10 files changed, 73 insertions(+), 16 deletions(-) diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index 7417c5dc0..5fc80bb9f 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -526,24 +526,29 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, /* Check Path */ if((parms->path == NULL) || (parms->path[0] == '\0')) { - if(parms->asset_name) + /* Check Asset Provided */ + if(!parms->asset_name) { - /* Generate Output Path */ - Asset* asset = dynamic_cast(LuaObject::getLuaObjectByName(parms->asset_name, Asset::OBJECT_TYPE)); - const char* path_prefix = StringLib::match(asset->getDriver(), "s3") ? "s3://" : ""; - const char* path_suffix = parms->as_geo ? ".geoparquet" : ".parquet"; - FString path_name("/%s.%016lX", id, OsApi::time(OsApi::CPU_CLK)); - FString path_str("%s%s%s%s", path_prefix, asset->getPath(), path_name.c_str(), path_suffix); - asset->releaseLuaObject(); - - /* Set Output Path */ - outputPath = path_str.c_str(true); - mlog(INFO, "Generating unique path: %s", outputPath); + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to determine output path without asset"); } - else + + /* Check Private Cluster */ + if(OsApi::getIsPublic()) { - throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to determine output path"); + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to stage output on public cluster"); } + + /* Generate Output Path */ + Asset* asset = dynamic_cast(LuaObject::getLuaObjectByName(parms->asset_name, Asset::OBJECT_TYPE)); + const char* path_prefix = StringLib::match(asset->getDriver(), "s3") ? "s3://" : ""; + const char* path_suffix = parms->as_geo ? ".geoparquet" : ".parquet"; + FString path_name("/%s.%016lX", id, OsApi::time(OsApi::CPU_CLK)); + FString path_str("%s%s%s%s", path_prefix, asset->getPath(), path_name.c_str(), path_suffix); + asset->releaseLuaObject(); + + /* Set Output Path */ + outputPath = path_str.c_str(true); + mlog(INFO, "Generating unique path: %s", outputPath); } else { diff --git a/packages/core/LuaLibrarySys.cpp b/packages/core/LuaLibrarySys.cpp index d98e1f8c9..5a1d3de0f 100644 --- a/packages/core/LuaLibrarySys.cpp +++ b/packages/core/LuaLibrarySys.cpp @@ -54,6 +54,7 @@ const struct luaL_Reg LuaLibrarySys::sysLibs [] = { {"metric", LuaLibrarySys::lsys_metric}, {"lsmsgq", LuaLibrarySys::lsys_lsmsgq}, {"setenvver", LuaLibrarySys::lsys_setenvver}, + {"setispublic", LuaLibrarySys::lsys_setispublic}, {"type", LuaLibrarySys::lsys_type}, {"setstddepth", LuaLibrarySys::lsys_setstddepth}, {"setiosz", LuaLibrarySys::lsys_setiosize}, @@ -299,6 +300,29 @@ int LuaLibrarySys::lsys_setenvver (lua_State* L) return 1; } +/*---------------------------------------------------------------------------- + * lsys_setispublic + *----------------------------------------------------------------------------*/ +int LuaLibrarySys::lsys_setispublic (lua_State* L) +{ + bool status = true; + const char* is_public_str = NULL; + if(lua_isstring(L, 1)) + { + is_public_str = lua_tostring(L, 1); + bool is_public = StringLib::match(is_public_str, "True"); + OsApi::setIsPublic(is_public); + } + else + { + mlog(CRITICAL, "Invalid parameter supplied to setting is_public, must be a string 'True' or 'False'"); + status = false; + } + + lua_pushboolean(L, status); + return 1; +} + /*---------------------------------------------------------------------------- * lsys_type - TODO - needs to handle userdata types *----------------------------------------------------------------------------*/ diff --git a/packages/core/LuaLibrarySys.h b/packages/core/LuaLibrarySys.h index 3641b3f4a..2fbc30ac5 100644 --- a/packages/core/LuaLibrarySys.h +++ b/packages/core/LuaLibrarySys.h @@ -79,6 +79,7 @@ class LuaLibrarySys static int lsys_metric (lua_State* L); static int lsys_lsmsgq (lua_State* L); static int lsys_setenvver (lua_State* L); + static int lsys_setispublic (lua_State* L); static int lsys_type (lua_State* L); static int lsys_setstddepth (lua_State* L); static int lsys_setiosize (lua_State* L); diff --git a/platforms/linux/OsApi.cpp b/platforms/linux/OsApi.cpp index 81d8eb51c..da6a1b30a 100644 --- a/platforms/linux/OsApi.cpp +++ b/platforms/linux/OsApi.cpp @@ -62,6 +62,7 @@ int OsApi::io_timeout = IO_DEFAULT_TIMEOUT; int OsApi::io_maxsize = IO_DEFAULT_MAXSIZE; int64_t OsApi::launch_time = 0; char* OsApi::environment_version = NULL; +bool OsApi::is_public = false; /****************************************************************************** * PUBLIC METHODS @@ -383,3 +384,19 @@ const char* OsApi::getEnvVersion (void) { return environment_version; } + +/*---------------------------------------------------------------------------- + * getEnvVersion + *----------------------------------------------------------------------------*/ +void OsApi::setIsPublic (bool _is_public) +{ + is_public = _is_public; +} + +/*---------------------------------------------------------------------------- + * getEnvVersion + *----------------------------------------------------------------------------*/ +bool OsApi::getIsPublic (void) +{ + return is_public; +} diff --git a/platforms/linux/OsApi.h b/platforms/linux/OsApi.h index 4b5e6ba38..416a5ccf7 100644 --- a/platforms/linux/OsApi.h +++ b/platforms/linux/OsApi.h @@ -228,6 +228,8 @@ class OsApi static int64_t getLaunchTime (void); static void setEnvVersion (const char* verstr); static const char* getEnvVersion (void); + static void setIsPublic (bool _is_public); + static bool getIsPublic (void); private: @@ -237,6 +239,7 @@ class OsApi static int io_maxsize; static int64_t launch_time; static char* environment_version; + static bool is_public; }; #endif /* __osapi__ */ diff --git a/scripts/apps/server.lua b/scripts/apps/server.lua index b1497435b..83665bf5f 100644 --- a/scripts/apps/server.lua +++ b/scripts/apps/server.lua @@ -39,6 +39,7 @@ local org_name = cfgtbl["cluster"] or os.getenv("CLUSTER") local ps_url = cfgtbl["provisioning_system"] or os.getenv("PROVISIONING_SYSTEM") local ps_auth = cfgtbl["authenticate_to_ps"] -- nil is false local container_registry = cfgtbl["container_registry"] or os.getenv("CONTAINER_REGISTRY") +local is_public = cfgtbl["is_public"] or os.getenv("IS_PUBLIC") or "False" -------------------------------------------------- -- System Configuration @@ -47,6 +48,9 @@ local container_registry = cfgtbl["container_registry"] or os.getenv("CON -- Set Environment Version -- sys.setenvver(environment_version) +-- Set Is Public -- +sys.setispublic(is_public) + -- Configure System Message Queue Depth -- sys.setstddepth(msgq_depth) diff --git a/targets/slideruleearth-aws/Makefile b/targets/slideruleearth-aws/Makefile index 5203ad93e..21390e5a0 100644 --- a/targets/slideruleearth-aws/Makefile +++ b/targets/slideruleearth-aws/Makefile @@ -172,7 +172,7 @@ sliderule: ## build the server using the local configuration make -C $(SWOT_BUILD_DIR) install run: ## run the server locally - IPV4=$(MYIP) ENVIRONMENT_VERSION=$(ENVVER) $(INSTALL_DIR)/bin/sliderule $(ROOT)/scripts/apps/server.lua config.json + IPV4=$(MYIP) ENVIRONMENT_VERSION=$(ENVVER) IS_PUBLIC=False $(INSTALL_DIR)/bin/sliderule $(ROOT)/scripts/apps/server.lua config.json run-buildenv: ## run the build environment docker container docker run -it -v $(ROOT):/host --rm --name buildenv $(ECR)/sliderule-buildenv:$(VERSION) diff --git a/targets/slideruleearth-aws/docker-compose.yml b/targets/slideruleearth-aws/docker-compose.yml index c09b504ea..5c6bda9de 100644 --- a/targets/slideruleearth-aws/docker-compose.yml +++ b/targets/slideruleearth-aws/docker-compose.yml @@ -18,6 +18,7 @@ services: environment: - IPV4=127.0.0.1 - ORCHESTRATOR=http://127.0.0.1:8050 + - IS_PUBLIC=False - CLUSTER=sliderule - PROVISIONING_SYSTEM=https://ps.localhost - CONTAINER_REGISTRY=742127912612.dkr.ecr.us-west-2.amazonaws.com diff --git a/targets/slideruleearth-aws/terraform/cluster/docker-compose-sliderule.yml b/targets/slideruleearth-aws/terraform/cluster/docker-compose-sliderule.yml index 16d7a251b..f63cef9bd 100644 --- a/targets/slideruleearth-aws/terraform/cluster/docker-compose-sliderule.yml +++ b/targets/slideruleearth-aws/terraform/cluster/docker-compose-sliderule.yml @@ -19,6 +19,7 @@ services: - IPV4=${IPV4} - ORCHESTRATOR=http://10.0.1.5:8050 - CLUSTER=$CLUSTER + - IS_PUBLIC=$IS_PUBLIC - PROVISIONING_SYSTEM=$PROVISIONING_SYSTEM - CONTAINER_REGISTRY=$CONTAINER_REGISTRY - ENVIRONMENT_VERSION=$ENVIRONMENT_VERSION diff --git a/targets/slideruleearth-aws/terraform/cluster/sliderule-asg.tf b/targets/slideruleearth-aws/terraform/cluster/sliderule-asg.tf index cc7fba69b..0fe263912 100644 --- a/targets/slideruleearth-aws/terraform/cluster/sliderule-asg.tf +++ b/targets/slideruleearth-aws/terraform/cluster/sliderule-asg.tf @@ -53,11 +53,12 @@ resource "aws_launch_configuration" "sliderule-instance" { #!/bin/bash export ENVIRONMENT_VERSION=${var.environment_version} export IPV4=$(hostname -I | awk '{print $1}') - aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 742127912612.dkr.ecr.us-west-2.amazonaws.com export CLUSTER=${var.cluster_name} + export IS_PUBLIC=${var.is_public} export SLIDERULE_IMAGE=${var.container_repo}/sliderule:${var.cluster_version} export PROVISIONING_SYSTEM="https://ps.${var.domain}" export CONTAINER_REGISTRY=${var.container_repo} + aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 742127912612.dkr.ecr.us-west-2.amazonaws.com aws s3 cp s3://sliderule/infrastructure/software/${var.cluster_name}-docker-compose-sliderule.yml ./docker-compose.yml docker-compose -p cluster up --detach EOF From e94161ae0506df39e66001341fe293e8dde1270a Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 22 Feb 2024 22:40:26 +0000 Subject: [PATCH 17/43] Fix for #379parsing a subfield of a record correctly propogates flags --- packages/arrow/ParquetBuilder.cpp | 9 ++++----- packages/core/RecordObject.cpp | 7 ++++--- packages/core/RecordObject.h | 4 ++-- plugins/icesat2/plugin/Atl03Reader.cpp | 10 +++++----- plugins/icesat2/plugin/Atl03Reader.h | 10 +++++----- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index 5fc80bb9f..e499c1468 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -428,10 +428,9 @@ struct ParquetBuilder::impl int ParquetBuilder::luaCreate (lua_State* L) { ArrowParms* _parms = NULL; - geo_data_t geo = { - .x_key = NULL, - .y_key = NULL - }; + geo_data_t geo; + geo.x_key = NULL; + geo.y_key = NULL; try { @@ -1207,7 +1206,7 @@ void ParquetBuilder::processRecordBatch (int num_rows) }; (void)builder.UnsafeAppend((uint8_t*)&point, sizeof(wkbpoint_t)); if(x_field.flags & RecordObject::BATCH) x_field.offset += batchRowSizeBytes * 8; - if(x_field.flags & RecordObject::BATCH) y_field.offset += batchRowSizeBytes * 8; + if(y_field.flags & RecordObject::BATCH) y_field.offset += batchRowSizeBytes * 8; } x_field.offset = starting_x_offset; y_field.offset = starting_y_offset; diff --git a/packages/core/RecordObject.cpp b/packages/core/RecordObject.cpp index 8fc9780c3..66dda5342 100644 --- a/packages/core/RecordObject.cpp +++ b/packages/core/RecordObject.cpp @@ -1503,11 +1503,11 @@ RecordObject::field_t RecordObject::getPointedToField(field_t f, bool allow_null /*---------------------------------------------------------------------------- * getUserField *----------------------------------------------------------------------------*/ -RecordObject::field_t RecordObject::getUserField (definition_t* def, const char* field_name) +RecordObject::field_t RecordObject::getUserField (definition_t* def, const char* field_name, uint32_t parent_flags) { assert(field_name); - field_t field = { INVALID_FIELD, 0, 0, NULL, NATIVE_FLAGS }; + field_t field = { INVALID_FIELD, 0, 0, NULL, parent_flags }; long element = -1; /* Sanity Check Def */ @@ -1585,7 +1585,7 @@ RecordObject::field_t RecordObject::getUserField (definition_t* def, const char* else { definition_t* subdef = definitions[field.exttype]; - field_t subfield = getUserField(subdef, subfield_name); + field_t subfield = getUserField(subdef, subfield_name, field.flags); subfield.offset += field.offset; field = subfield; } @@ -1596,6 +1596,7 @@ RecordObject::field_t RecordObject::getUserField (definition_t* def, const char* } /* Return Field */ + field.flags |= parent_flags; return field; } diff --git a/packages/core/RecordObject.h b/packages/core/RecordObject.h index a817f452f..afc84ff16 100644 --- a/packages/core/RecordObject.h +++ b/packages/core/RecordObject.h @@ -126,7 +126,7 @@ class RecordObject int32_t offset; // bits for BITFIELD, bytes for everything else int32_t elements; const char* exttype; - uint64_t flags; + uint64_t flags; // 64-bits provided here to consume padding } fieldDef_t; typedef enum { @@ -328,7 +328,7 @@ class RecordObject /* Regular Methods */ field_t getPointedToField (field_t field, bool allow_null, int element=0); - static field_t getUserField (definition_t* def, const char* field_name); + static field_t getUserField (definition_t* def, const char* field_name, uint32_t parent_flags=NATIVE_FLAGS); static recordDefErr_t addDefinition (definition_t** rec_def, const char* rec_type, const char* id_field, int data_size, const fieldDef_t* fields, int num_fields, int max_fields); static recordDefErr_t addField (definition_t* def, const char* field_name, fieldType_t type, int offset, int elements, const char* exttype, unsigned int flags); diff --git a/plugins/icesat2/plugin/Atl03Reader.cpp b/plugins/icesat2/plugin/Atl03Reader.cpp index a68dad835..6acd7eac4 100644 --- a/plugins/icesat2/plugin/Atl03Reader.cpp +++ b/plugins/icesat2/plugin/Atl03Reader.cpp @@ -69,7 +69,7 @@ const RecordObject::fieldDef_t Atl03Reader::exRecDef[] = { {"pair", RecordObject::UINT8, offsetof(extent_t, pair), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"sc_orient", RecordObject::UINT8, offsetof(extent_t, spacecraft_orientation), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"rgt", RecordObject::UINT16, offsetof(extent_t, reference_ground_track), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, - {"cycle", RecordObject::UINT16, offsetof(extent_t, cycle), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, + {"cycle", RecordObject::UINT8, offsetof(extent_t, cycle), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"segment_id", RecordObject::UINT32, offsetof(extent_t, segment_id), 1, NULL, NATIVE_FLAGS}, {"segment_dist", RecordObject::DOUBLE, offsetof(extent_t, segment_distance), 1, NULL, NATIVE_FLAGS}, // distance from equator {"background_rate", RecordObject::DOUBLE, offsetof(extent_t, background_rate), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, @@ -1750,7 +1750,7 @@ void Atl03Reader::postRecord (RecordObject& record, stats_t& local_stats) * vvv - version * ee - revision *----------------------------------------------------------------------------*/ -void Atl03Reader::parseResource (const char* _resource, int32_t& rgt, int32_t& cycle, int32_t& region) +void Atl03Reader::parseResource (const char* _resource, uint16_t& rgt, uint8_t& cycle, uint8_t& region) { if(StringLib::size(_resource) < 29) { @@ -1769,7 +1769,7 @@ void Atl03Reader::parseResource (const char* _resource, int32_t& rgt, int32_t& c rgt_str[4] = '\0'; if(StringLib::str2long(rgt_str, &val, 10)) { - rgt = val; + rgt = (uint16_t)val; } else { @@ -1782,7 +1782,7 @@ void Atl03Reader::parseResource (const char* _resource, int32_t& rgt, int32_t& c cycle_str[2] = '\0'; if(StringLib::str2long(cycle_str, &val, 10)) { - cycle = val; + cycle = (uint8_t)val; } else { @@ -1795,7 +1795,7 @@ void Atl03Reader::parseResource (const char* _resource, int32_t& rgt, int32_t& c region_str[2] = '\0'; if(StringLib::str2long(region_str, &val, 10)) { - region = val; + region = (uint8_t)val; } else { diff --git a/plugins/icesat2/plugin/Atl03Reader.h b/plugins/icesat2/plugin/Atl03Reader.h index 2fa1a749d..ca9bb0b11 100644 --- a/plugins/icesat2/plugin/Atl03Reader.h +++ b/plugins/icesat2/plugin/Atl03Reader.h @@ -102,7 +102,7 @@ class Atl03Reader: public LuaObject uint8_t pair; // 0 (l), 1 (r) uint8_t spacecraft_orientation; // sc_orient_t uint16_t reference_ground_track; - uint16_t cycle; + uint8_t cycle; uint32_t segment_id; uint32_t photon_count; float solar_elevation; @@ -309,9 +309,9 @@ class Atl03Reader: public LuaObject H5Coro::context_t context; // for ATL03 file H5Coro::context_t context08; // for ATL08 file - int32_t start_rgt; - int32_t start_cycle; - int32_t start_region; + uint16_t start_rgt; + uint8_t start_cycle; + uint8_t start_region; /*-------------------------------------------------------------------- * Methods @@ -327,7 +327,7 @@ class Atl03Reader: public LuaObject void generateExtentRecord (uint64_t extent_id, info_t* info, TrackState& state, const Atl03Data& atl03, vector& rec_list, int& total_size); static void generateAncillaryRecords (uint64_t extent_id, AncillaryFields::list_t* field_list, H5DArrayDictionary* field_dict, AncillaryFields::type_t type, List* indices, vector& rec_list, int& total_size); void postRecord (RecordObject& record, stats_t& local_stats); - static void parseResource (const char* resource, int32_t& rgt, int32_t& cycle, int32_t& region); + static void parseResource (const char* resource, uint16_t& rgt, uint8_t& cycle, uint8_t& region); static int luaParms (lua_State* L); static int luaStats (lua_State* L); From 407da7d6bedf27d898bc0f5939321af6686b3005 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Fri, 23 Feb 2024 14:19:33 +0000 Subject: [PATCH 18/43] fixed bug in atl03 above classifier code where segment id was being used instead of segment index --- plugins/icesat2/plugin/Atl03Reader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/icesat2/plugin/Atl03Reader.cpp b/plugins/icesat2/plugin/Atl03Reader.cpp index 6acd7eac4..872f8d8be 100644 --- a/plugins/icesat2/plugin/Atl03Reader.cpp +++ b/plugins/icesat2/plugin/Atl03Reader.cpp @@ -733,7 +733,7 @@ void Atl03Reader::Atl08Class::classify (info_t* info, const Region& region, cons if(info->reader->parms->phoreal.above_classifier && (classification[atl03_photon] != Icesat2Parms::ATL08_TOP_OF_CANOPY)) { uint8_t spot = Icesat2Parms::getSpotNumber((Icesat2Parms::sc_orient_t)atl03.sc_orient[0], (Icesat2Parms::track_t)info->track, info->pair); - if( (atl03.solar_elevation[atl03_segment] <= 5.0) && + if( (atl03.solar_elevation[atl03_segment_index] <= 5.0) && ((spot == 1) || (spot == 3) || (spot == 5)) && (atl03.signal_conf_ph[atl03_photon] == Icesat2Parms::CNF_SURFACE_HIGH) && ((relief[atl03_photon] >= 0.0) && (relief[atl03_photon] < 35.0)) ) From 535deaff4bc4d9d0b5ed917478e4f61b1028f4e0 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Fri, 23 Feb 2024 22:04:23 +0000 Subject: [PATCH 19/43] added polygon as supported type for region; allow write access to sliderule-public --- clients/python/sliderule/sliderule.py | 7 +++++++ .../slideruleearth-aws/terraform/cluster/s3-policy.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/clients/python/sliderule/sliderule.py b/clients/python/sliderule/sliderule.py index e0fcf0ca8..584a2f57d 100644 --- a/clients/python/sliderule/sliderule.py +++ b/clients/python/sliderule/sliderule.py @@ -1355,6 +1355,13 @@ def toregion(source, tolerance=0.0, cellsize=0.01, n_clusters=1): datafile = file.read() os.remove(tempfile) + elif isinstance(source, Polygon): + gdf = geopandas.GeoDataFrame(geometry=[source], crs=EPSG_WGS84) + gdf.to_file(tempfile, driver="GeoJSON") + with open(tempfile, mode='rt') as file: + datafile = file.read() + os.remove(tempfile) + elif isinstance(source, list) and (len(source) >= 4) and (len(source) % 2 == 0): # create lat/lon lists if len(source) == 4: # bounding box diff --git a/targets/slideruleearth-aws/terraform/cluster/s3-policy.json b/targets/slideruleearth-aws/terraform/cluster/s3-policy.json index 83fa0626d..ac807c5eb 100644 --- a/targets/slideruleearth-aws/terraform/cluster/s3-policy.json +++ b/targets/slideruleearth-aws/terraform/cluster/s3-policy.json @@ -14,7 +14,7 @@ { "Effect": "Allow", "Action": ["s3:PutObject"], - "Resource": ["arn:aws:s3:::sliderule/logs/*"] + "Resource": ["arn:aws:s3:::sliderule/logs/*", "arn:aws:s3:::sliderule-public/*"] } ] } From 2cc8309cd5d27d8ad45be9ae6842b3c191f9bbfe Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Mon, 26 Feb 2024 17:47:01 +0000 Subject: [PATCH 20/43] parquet support for nested arrays --- packages/arrow/ParquetBuilder.cpp | 1475 ++++++++++++++++++----------- 1 file changed, 902 insertions(+), 573 deletions(-) diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index e499c1468..fb4608dd7 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -416,388 +416,176 @@ struct ParquetBuilder::impl /* Append Meta String */ metadata->Append("pandas", pandasstr.c_str()); } -}; - -/****************************************************************************** - * PUBLIC METHODS - ******************************************************************************/ - -/*---------------------------------------------------------------------------- - * luaCreate - :parquet(, , , , [, ], []) - *----------------------------------------------------------------------------*/ -int ParquetBuilder::luaCreate (lua_State* L) -{ - ArrowParms* _parms = NULL; - geo_data_t geo; - geo.x_key = NULL; - geo.y_key = NULL; - try + /*---------------------------------------------------------------------------- + * processField + *----------------------------------------------------------------------------*/ + static void processField (RecordObject::field_t& field, shared_ptr* column, Ordering& record_batch, int num_rows, int batch_row_size_bits) { - /* Get Parameters */ - _parms = dynamic_cast(getLuaObject(L, 1, ArrowParms::OBJECT_TYPE)); - const char* outq_name = getLuaString(L, 2); - const char* inq_name = getLuaString(L, 3); - const char* rec_type = getLuaString(L, 4); - const char* id = getLuaString(L, 5); - const char* x_key = getLuaString(L, 6, true, NULL); - const char* y_key = getLuaString(L, 7, true, NULL); - const char* index_key = getLuaString(L, 8, true, NULL); + batch_t batch; - /* Build Geometry Fields */ - geo.as_geo = _parms->as_geo; - if(geo.as_geo && (x_key != NULL) && (y_key != NULL)) + switch(field.type) { - geo.x_field = RecordObject::getDefinedField(rec_type, x_key); - if(geo.x_field.type == RecordObject::INVALID_FIELD) - { - throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to extract x field [%s] from record type <%s>", x_key, rec_type); - } - - geo.y_field = RecordObject::getDefinedField(rec_type, y_key); - if(geo.y_field.type == RecordObject::INVALID_FIELD) + case RecordObject::DOUBLE: { - throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to extract y field [%s] from record type <%s>", y_key, rec_type); + arrow::DoubleBuilder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((double)batch.record->getValueReal(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + float value = (float)batch.record->getValueReal(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; } - geo.x_key = StringLib::duplicate(x_key); - geo.y_key = StringLib::duplicate(y_key); - } - else - { - /* Unable to Create GeoParquet */ - geo.as_geo = false; - } - - /* Create Dispatch */ - return createLuaObject(L, new ParquetBuilder(L, _parms, outq_name, inq_name, rec_type, id, geo, index_key)); - } - catch(const RunTimeException& e) - { - if(_parms) _parms->releaseLuaObject(); - delete [] geo.x_key; - delete [] geo.y_key; - mlog(e.level(), "Error creating %s: %s", LUA_META_NAME, e.what()); - return returnLuaStatus(L, false); - } -} - -/*---------------------------------------------------------------------------- - * init - *----------------------------------------------------------------------------*/ -void ParquetBuilder::init (void) -{ - RECDEF(metaRecType, metaRecDef, sizeof(arrow_file_meta_t), NULL); - RECDEF(dataRecType, dataRecDef, sizeof(arrow_file_data_t), NULL); - RECDEF(remoteRecType, remoteRecDef, sizeof(arrow_file_remote_t), NULL); -} - -/*---------------------------------------------------------------------------- - * deinit - *----------------------------------------------------------------------------*/ -void ParquetBuilder::deinit (void) -{ -} - -/****************************************************************************** - * PRIVATE METHODS - *******************************************************************************/ - -/*---------------------------------------------------------------------------- - * Constructor - *----------------------------------------------------------------------------*/ -ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, - const char* outq_name, const char* inq_name, - const char* rec_type, const char* id, const geo_data_t& geo, const char* index_key): - LuaObject(L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), - parms(_parms), - recType(StringLib::duplicate(rec_type)), - batchRecType(NULL), - fieldList(LIST_BLOCK_SIZE), - geoData(geo) -{ - assert(_parms); - assert(outq_name); - assert(inq_name); - assert(rec_type); - assert(id); - - /* Check Path */ - if((parms->path == NULL) || (parms->path[0] == '\0')) - { - /* Check Asset Provided */ - if(!parms->asset_name) - { - throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to determine output path without asset"); - } - - /* Check Private Cluster */ - if(OsApi::getIsPublic()) - { - throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to stage output on public cluster"); - } - - /* Generate Output Path */ - Asset* asset = dynamic_cast(LuaObject::getLuaObjectByName(parms->asset_name, Asset::OBJECT_TYPE)); - const char* path_prefix = StringLib::match(asset->getDriver(), "s3") ? "s3://" : ""; - const char* path_suffix = parms->as_geo ? ".geoparquet" : ".parquet"; - FString path_name("/%s.%016lX", id, OsApi::time(OsApi::CPU_CLK)); - FString path_str("%s%s%s%s", path_prefix, asset->getPath(), path_name.c_str(), path_suffix); - asset->releaseLuaObject(); - - /* Set Output Path */ - outputPath = path_str.c_str(true); - mlog(INFO, "Generating unique path: %s", outputPath); - } - else - { - outputPath = StringLib::duplicate(parms->path); - } - - /* Allocate Private Implementation */ - pimpl = new ParquetBuilder::impl; - - /* Define Table Schema */ - vector> schema_vector; - ParquetBuilder::impl::addFieldsToSchema(schema_vector, fieldList, &batchRecType, geoData, rec_type, 0, 0); - if(geoData.as_geo) schema_vector.push_back(arrow::field("geometry", arrow::binary())); - pimpl->schema = make_shared(schema_vector); - fieldIterator = new field_iterator_t(fieldList); - - /* Row Based Parameters */ - batchRowSizeBytes = RecordObject::getRecordDataSize(batchRecType); - rowSizeBytes = RecordObject::getRecordDataSize(rec_type) + batchRowSizeBytes; - maxRowsInGroup = ROW_GROUP_SIZE / rowSizeBytes; - - /* Initialize Queues */ - int qdepth = maxRowsInGroup * QUEUE_BUFFER_FACTOR; - outQ = new Publisher(outq_name, Publisher::defaultFree, qdepth); - inQ = new Subscriber(inq_name, MsgQ::SUBSCRIBER_OF_CONFIDENCE, qdepth); - - /* Create Unique Temporary Filename */ - FString tmp_file("%s%s.parquet", TMP_FILE_PREFIX, id); - fileName = tmp_file.c_str(true); - - /* Create Arrow Output Stream */ - shared_ptr file_output_stream; - PARQUET_ASSIGN_OR_THROW(file_output_stream, arrow::io::FileOutputStream::Open(fileName)); - - /* Create Writer Properties */ - parquet::WriterProperties::Builder writer_props_builder; - writer_props_builder.compression(parquet::Compression::SNAPPY); - writer_props_builder.version(parquet::ParquetVersion::PARQUET_2_6); - shared_ptr writer_props = writer_props_builder.build(); - - /* Create Arrow Writer Properties */ - auto arrow_writer_props = parquet::ArrowWriterProperties::Builder().store_schema()->build(); - - /* Build GeoParquet MetaData */ - auto metadata = pimpl->schema->metadata() ? pimpl->schema->metadata()->Copy() : std::make_shared(); - if(geoData.as_geo) ParquetBuilder::impl::appendGeoMetaData(metadata); - ParquetBuilder::impl::appendServerMetaData(metadata); - ParquetBuilder::impl::appendPandasMetaData(metadata, pimpl->schema, fieldIterator, index_key, geoData.as_geo); - pimpl->schema = pimpl->schema->WithMetadata(metadata); - - /* Create Parquet Writer */ - #ifdef APACHE_ARROW_10_COMPAT - (void)parquet::arrow::FileWriter::Open(*pimpl->schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props, &pimpl->parquetWriter); - #elif 0 // alternative method of creating file writer - std::shared_ptr parquet_schema; - (void)parquet::arrow::ToParquetSchema(pimpl->schema.get(), *writer_props, *arrow_writer_props, &parquet_schema); - auto schema_node = std::static_pointer_cast(parquet_schema->schema_root()); - std::unique_ptr base_writer; - base_writer = parquet::ParquetFileWriter::Open(std::move(file_output_stream), schema_node, std::move(writer_props), metadata); - auto schema_ptr = std::make_shared<::arrow::Schema>(*pimpl->schema); - (void)parquet::arrow::FileWriter::Make(::arrow::default_memory_pool(), std::move(base_writer), std::move(schema_ptr), std::move(arrow_writer_props), &pimpl->parquetWriter); - #else - arrow::Result> result = parquet::arrow::FileWriter::Open(*pimpl->schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props); - if(result.ok()) pimpl->parquetWriter = std::move(result).ValueOrDie(); - else mlog(CRITICAL, "Failed to open parquet writer: %s", result.status().ToString().c_str()); - #endif - - /* Start Builder Thread */ - active = true; - builderPid = new Thread(builderThread, this); -} - -/*---------------------------------------------------------------------------- - * Destructor - - *----------------------------------------------------------------------------*/ -ParquetBuilder::~ParquetBuilder(void) -{ - active = false; - delete builderPid; - parms->releaseLuaObject(); - delete [] fileName; - delete [] outputPath; - delete [] recType; - delete outQ; - delete inQ; - delete fieldIterator; - delete pimpl; - if(geoData.as_geo) - { - delete [] geoData.x_key; - delete [] geoData.y_key; - } -} - -/*---------------------------------------------------------------------------- - * builderThread - *----------------------------------------------------------------------------*/ -void* ParquetBuilder::builderThread(void* parm) -{ - ParquetBuilder* builder = static_cast(parm); - int row_cnt = 0; - - /* Early Exit on No Writer */ - if(!builder->pimpl->parquetWriter) - { - return NULL; - } - - /* Start Trace */ - uint32_t trace_id = start_trace(INFO, builder->traceId, "parquet_builder", "{\"filename\":\"%s\"}", builder->fileName); - EventLib::stashId(trace_id); - - /* Loop Forever */ - while(builder->active) - { - /* Receive Message */ - Subscriber::msgRef_t ref; - int recv_status = builder->inQ->receiveRef(ref, SYS_TIMEOUT); - if(recv_status > 0) - { - /* Process Record */ - if(ref.size > 0) + case RecordObject::FLOAT: { - /* Get Record and Match to Type being Processed */ - RecordInterface* record = new RecordInterface((unsigned char*)ref.data, ref.size); - if(!StringLib::match(record->getRecordType(), builder->recType)) + arrow::FloatBuilder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) { - delete record; - builder->outQ->postCopy(ref.data, ref.size); - builder->inQ->dereference(ref); - continue; + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((float)batch.record->getValueReal(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + float value = (float)batch.record->getValueReal(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); } + (void)builder.Finish(column); + break; + } - /* Determine Rows in Record */ - int record_size_bytes = record->getAllocatedDataSize(); - int batch_size_bytes = record_size_bytes - (builder->rowSizeBytes - builder->batchRowSizeBytes); - int num_rows = batch_size_bytes / builder->batchRowSizeBytes; - int left_over = batch_size_bytes % builder->batchRowSizeBytes; - if(left_over > 0) + case RecordObject::INT8: + { + arrow::Int8Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) { - mlog(ERROR, "Invalid record size received for %s: %d %% %d != 0", record->getRecordType(), batch_size_bytes, builder->batchRowSizeBytes); - delete record; // record is not batched, so must delete here - builder->inQ->dereference(ref); // record is not batched, so must dereference here - continue; + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((int8_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + int8_t value = (int8_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); } + (void)builder.Finish(column); + break; + } - /* Create Batch Structure */ - batch_t batch = { - .ref = ref, - .record = record, - .rows = num_rows - }; - - /* Add Batch to Ordering */ - builder->recordBatch.add(row_cnt, batch); - row_cnt += num_rows; - if(row_cnt >= builder->maxRowsInGroup) + case RecordObject::INT16: + { + arrow::Int16Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) { - builder->processRecordBatch(row_cnt); - row_cnt = 0; + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((int16_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + int16_t value = (int16_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); } - } - else - { - /* Terminating Message */ - mlog(DEBUG, "Terminator received on %s, exiting parquet builder", builder->inQ->getName()); - builder->active = false; // breaks out of loop - builder->inQ->dereference(ref); // terminator is not batched, so must dereference here - } - } - else if(recv_status != MsgQ::STATE_TIMEOUT) - { - /* Break Out on Failure */ - mlog(CRITICAL, "Failed queue receive on %s with error %d", builder->inQ->getName(), recv_status); - builder->active = false; // breaks out of loop - } - } - - /* Process Remaining Records */ - builder->processRecordBatch(row_cnt); - - /* Close Parquet Writer */ - (void)builder->pimpl->parquetWriter->Close(); - - /* Send File to User */ - const char* _path = builder->outputPath; - uint32_t send_trace_id = start_trace(INFO, trace_id, "send_file", "{\"path\": \"%s\"}", _path); - int _path_len = StringLib::size(_path); - if( (_path_len > 5) && - (_path[0] == 's') && - (_path[1] == '3') && - (_path[2] == ':') && - (_path[3] == '/') && - (_path[4] == '/')) - { - /* Upload File to S3 */ - builder->send2S3(&_path[5]); - } - else - { - /* Stream File Back to Client */ - builder->send2Client(); - } - - /* Remove File */ - int rc = remove(builder->fileName); - if(rc != 0) - { - mlog(CRITICAL, "Failed (%d) to delete file %s: %s", rc, builder->fileName, strerror(errno)); - } - - stop_trace(INFO, send_trace_id); - - /* Signal Completion */ - builder->signalComplete(); - - /* Stop Trace */ - stop_trace(INFO, trace_id); - - /* Exit Thread */ - return NULL; -} - -/*---------------------------------------------------------------------------- - * processRecordBatch - *----------------------------------------------------------------------------*/ -void ParquetBuilder::processRecordBatch (int num_rows) -{ - batch_t batch; - - /* Start Trace */ - uint32_t parent_trace_id = EventLib::grabId(); - uint32_t trace_id = start_trace(INFO, parent_trace_id, "process_batch", "{\"num_rows\": %d}", num_rows); + (void)builder.Finish(column); + break; + } - /* Loop Through Fields in Schema */ - vector> columns; - for(int i = 0; i < fieldIterator->length; i++) - { - uint32_t field_trace_id = start_trace(INFO, trace_id, "append_field", "{\"field\": %d}", i); - RecordObject::field_t field = (*fieldIterator)[i]; - shared_ptr column; + case RecordObject::INT32: + { + arrow::Int32Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((int32_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + int32_t value = (int32_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } - /* Loop Through Each Row */ - switch(field.type) - { - case RecordObject::DOUBLE: + case RecordObject::INT64: { - arrow::DoubleBuilder builder; + arrow::Int64Builder builder; (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); + unsigned long key = record_batch.first(&batch); while(key != (unsigned long)INVALID_KEY) { if(field.flags & RecordObject::BATCH) @@ -805,30 +593,30 @@ void ParquetBuilder::processRecordBatch (int num_rows) int32_t starting_offset = field.offset; for(int row = 0; row < batch.rows; row++) { - builder.UnsafeAppend((double)batch.record->getValueReal(field)); - field.offset += batchRowSizeBytes * 8; + builder.UnsafeAppend((int64_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - double value = (double)batch.record->getValueReal(field); + int64_t value = (int64_t)batch.record->getValueInteger(field); for(int row = 0; row < batch.rows; row++) { builder.UnsafeAppend(value); } } - key = recordBatch.next(&batch); + key = record_batch.next(&batch); } - (void)builder.Finish(&column); + (void)builder.Finish(column); break; } - case RecordObject::FLOAT: + case RecordObject::UINT8: { - arrow::FloatBuilder builder; + arrow::UInt8Builder builder; (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); + unsigned long key = record_batch.first(&batch); while(key != (unsigned long)INVALID_KEY) { if(field.flags & RecordObject::BATCH) @@ -836,30 +624,30 @@ void ParquetBuilder::processRecordBatch (int num_rows) int32_t starting_offset = field.offset; for(int row = 0; row < batch.rows; row++) { - builder.UnsafeAppend((float)batch.record->getValueReal(field)); - field.offset += batchRowSizeBytes * 8; + builder.UnsafeAppend((uint8_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - float value = (float)batch.record->getValueReal(field); + uint8_t value = (uint8_t)batch.record->getValueInteger(field); for(int row = 0; row < batch.rows; row++) { builder.UnsafeAppend(value); } } - key = recordBatch.next(&batch); + key = record_batch.next(&batch); } - (void)builder.Finish(&column); + (void)builder.Finish(column); break; } - case RecordObject::INT8: + case RecordObject::UINT16: { - arrow::Int8Builder builder; + arrow::UInt16Builder builder; (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); + unsigned long key = record_batch.first(&batch); while(key != (unsigned long)INVALID_KEY) { if(field.flags & RecordObject::BATCH) @@ -867,30 +655,30 @@ void ParquetBuilder::processRecordBatch (int num_rows) int32_t starting_offset = field.offset; for(int row = 0; row < batch.rows; row++) { - builder.UnsafeAppend((int8_t)batch.record->getValueInteger(field)); - field.offset += batchRowSizeBytes * 8; + builder.UnsafeAppend((uint16_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - int8_t value = (int8_t)batch.record->getValueInteger(field); + uint16_t value = (uint16_t)batch.record->getValueInteger(field); for(int row = 0; row < batch.rows; row++) { builder.UnsafeAppend(value); } } - key = recordBatch.next(&batch); + key = record_batch.next(&batch); } - (void)builder.Finish(&column); + (void)builder.Finish(column); break; } - case RecordObject::INT16: + case RecordObject::UINT32: { - arrow::Int16Builder builder; + arrow::UInt32Builder builder; (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); + unsigned long key = record_batch.first(&batch); while(key != (unsigned long)INVALID_KEY) { if(field.flags & RecordObject::BATCH) @@ -898,30 +686,30 @@ void ParquetBuilder::processRecordBatch (int num_rows) int32_t starting_offset = field.offset; for(int row = 0; row < batch.rows; row++) { - builder.UnsafeAppend((int16_t)batch.record->getValueInteger(field)); - field.offset += batchRowSizeBytes * 8; + builder.UnsafeAppend((uint32_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - int16_t value = (int16_t)batch.record->getValueInteger(field); + uint32_t value = (uint32_t)batch.record->getValueInteger(field); for(int row = 0; row < batch.rows; row++) { builder.UnsafeAppend(value); } } - key = recordBatch.next(&batch); + key = record_batch.next(&batch); } - (void)builder.Finish(&column); + (void)builder.Finish(column); break; } - case RecordObject::INT32: + case RecordObject::UINT64: { - arrow::Int32Builder builder; + arrow::UInt64Builder builder; (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); + unsigned long key = record_batch.first(&batch); while(key != (unsigned long)INVALID_KEY) { if(field.flags & RecordObject::BATCH) @@ -929,30 +717,30 @@ void ParquetBuilder::processRecordBatch (int num_rows) int32_t starting_offset = field.offset; for(int row = 0; row < batch.rows; row++) { - builder.UnsafeAppend((int32_t)batch.record->getValueInteger(field)); - field.offset += batchRowSizeBytes * 8; + builder.UnsafeAppend((uint64_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - int32_t value = (int32_t)batch.record->getValueInteger(field); + uint64_t value = (uint64_t)batch.record->getValueInteger(field); for(int row = 0; row < batch.rows; row++) { builder.UnsafeAppend(value); } } - key = recordBatch.next(&batch); + key = record_batch.next(&batch); } - (void)builder.Finish(&column); + (void)builder.Finish(column); break; } - case RecordObject::INT64: + case RecordObject::TIME8: { - arrow::Int64Builder builder; + arrow::TimestampBuilder builder(arrow::timestamp(arrow::TimeUnit::NANO), arrow::default_memory_pool()); (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); + unsigned long key = record_batch.first(&batch); while(key != (unsigned long)INVALID_KEY) { if(field.flags & RecordObject::BATCH) @@ -961,7 +749,7 @@ void ParquetBuilder::processRecordBatch (int num_rows) for(int row = 0; row < batch.rows; row++) { builder.UnsafeAppend((int64_t)batch.record->getValueInteger(field)); - field.offset += batchRowSizeBytes * 8; + field.offset += batch_row_size_bits; } field.offset = starting_offset; } @@ -970,207 +758,741 @@ void ParquetBuilder::processRecordBatch (int num_rows) int64_t value = (int64_t)batch.record->getValueInteger(field); for(int row = 0; row < batch.rows; row++) { - builder.UnsafeAppend(value); + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::STRING: + { + arrow::StringBuilder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + const char* str = batch.record->getValueText(field); + builder.UnsafeAppend(str, StringLib::size(str)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + const char* str = batch.record->getValueText(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(str, StringLib::size(str)); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + default: + { + break; + } + } + } + + /*---------------------------------------------------------------------------- + * processArray + *----------------------------------------------------------------------------*/ + static void processArray (RecordObject::field_t& field, shared_ptr* column, Ordering& record_batch, int batch_row_size_bits) + { + batch_t batch; + + if(!(field.flags & RecordObject::BATCH)) + { + batch_row_size_bits = 0; + } + + switch(field.type) + { + case RecordObject::DOUBLE: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((double)batch.record->getValueReal(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::FLOAT: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((float)batch.record->getValueReal(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::INT8: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((int8_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::INT16: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((int16_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::INT32: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((int32_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::INT64: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((int64_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::UINT8: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((uint8_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::UINT16: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((uint16_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::UINT32: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((uint32_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::UINT64: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((uint64_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::TIME8: + { + auto builder = make_shared(arrow::timestamp(arrow::TimeUnit::NANO), arrow::default_memory_pool()); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((int64_t)batch.record->getValueInteger(field, element)); } + field.offset += batch_row_size_bits; } - key = recordBatch.next(&batch); + field.offset = starting_offset; + key = record_batch.next(&batch); } - (void)builder.Finish(&column); + (void)list_builder.Finish(column); break; } - case RecordObject::UINT8: + case RecordObject::STRING: { - arrow::UInt8Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); while(key != (unsigned long)INVALID_KEY) { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((uint8_t)batch.record->getValueInteger(field)); - field.offset += batchRowSizeBytes * 8; - } - field.offset = starting_offset; - } - else // non-batch field + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) { - uint8_t value = (uint8_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) { - builder.UnsafeAppend(value); + const char* str = batch.record->getValueText(field, NULL, element); + (void)builder->Append(str, StringLib::size(str)); } + field.offset += batch_row_size_bits; } - key = recordBatch.next(&batch); + field.offset = starting_offset; + key = record_batch.next(&batch); } - (void)builder.Finish(&column); + (void)list_builder.Finish(column); + break; + } + + default: + { break; } + } + } + +}; + +/****************************************************************************** + * PUBLIC METHODS + ******************************************************************************/ + +/*---------------------------------------------------------------------------- + * luaCreate - :parquet(, , , , [, ], []) + *----------------------------------------------------------------------------*/ +int ParquetBuilder::luaCreate (lua_State* L) +{ + ArrowParms* _parms = NULL; + geo_data_t geo; + geo.x_key = NULL; + geo.y_key = NULL; + + try + { + /* Get Parameters */ + _parms = dynamic_cast(getLuaObject(L, 1, ArrowParms::OBJECT_TYPE)); + const char* outq_name = getLuaString(L, 2); + const char* inq_name = getLuaString(L, 3); + const char* rec_type = getLuaString(L, 4); + const char* id = getLuaString(L, 5); + const char* x_key = getLuaString(L, 6, true, NULL); + const char* y_key = getLuaString(L, 7, true, NULL); + const char* index_key = getLuaString(L, 8, true, NULL); + + /* Build Geometry Fields */ + geo.as_geo = _parms->as_geo; + if(geo.as_geo && (x_key != NULL) && (y_key != NULL)) + { + geo.x_field = RecordObject::getDefinedField(rec_type, x_key); + if(geo.x_field.type == RecordObject::INVALID_FIELD) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to extract x field [%s] from record type <%s>", x_key, rec_type); + } + + geo.y_field = RecordObject::getDefinedField(rec_type, y_key); + if(geo.y_field.type == RecordObject::INVALID_FIELD) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to extract y field [%s] from record type <%s>", y_key, rec_type); + } + + geo.x_key = StringLib::duplicate(x_key); + geo.y_key = StringLib::duplicate(y_key); + } + else + { + /* Unable to Create GeoParquet */ + geo.as_geo = false; + } + + /* Create Dispatch */ + return createLuaObject(L, new ParquetBuilder(L, _parms, outq_name, inq_name, rec_type, id, geo, index_key)); + } + catch(const RunTimeException& e) + { + if(_parms) _parms->releaseLuaObject(); + delete [] geo.x_key; + delete [] geo.y_key; + mlog(e.level(), "Error creating %s: %s", LUA_META_NAME, e.what()); + return returnLuaStatus(L, false); + } +} + +/*---------------------------------------------------------------------------- + * init + *----------------------------------------------------------------------------*/ +void ParquetBuilder::init (void) +{ + RECDEF(metaRecType, metaRecDef, sizeof(arrow_file_meta_t), NULL); + RECDEF(dataRecType, dataRecDef, sizeof(arrow_file_data_t), NULL); + RECDEF(remoteRecType, remoteRecDef, sizeof(arrow_file_remote_t), NULL); +} + +/*---------------------------------------------------------------------------- + * deinit + *----------------------------------------------------------------------------*/ +void ParquetBuilder::deinit (void) +{ +} + +/****************************************************************************** + * PRIVATE METHODS + *******************************************************************************/ + +/*---------------------------------------------------------------------------- + * Constructor + *----------------------------------------------------------------------------*/ +ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, + const char* outq_name, const char* inq_name, + const char* rec_type, const char* id, const geo_data_t& geo, const char* index_key): + LuaObject(L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), + parms(_parms), + recType(StringLib::duplicate(rec_type)), + batchRecType(NULL), + fieldList(LIST_BLOCK_SIZE), + geoData(geo) +{ + assert(_parms); + assert(outq_name); + assert(inq_name); + assert(rec_type); + assert(id); + + /* Check Path */ + if((parms->path == NULL) || (parms->path[0] == '\0')) + { + /* Check Asset Provided */ + if(!parms->asset_name) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to determine output path without asset"); + } + + /* Check Private Cluster */ + if(OsApi::getIsPublic()) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to stage output on public cluster"); + } + + /* Generate Output Path */ + Asset* asset = dynamic_cast(LuaObject::getLuaObjectByName(parms->asset_name, Asset::OBJECT_TYPE)); + const char* path_prefix = StringLib::match(asset->getDriver(), "s3") ? "s3://" : ""; + const char* path_suffix = parms->as_geo ? ".geoparquet" : ".parquet"; + FString path_name("/%s.%016lX", id, OsApi::time(OsApi::CPU_CLK)); + FString path_str("%s%s%s%s", path_prefix, asset->getPath(), path_name.c_str(), path_suffix); + asset->releaseLuaObject(); + + /* Set Output Path */ + outputPath = path_str.c_str(true); + mlog(INFO, "Generating unique path: %s", outputPath); + } + else + { + outputPath = StringLib::duplicate(parms->path); + } + + /* Allocate Private Implementation */ + pimpl = new ParquetBuilder::impl; + + /* Define Table Schema */ + vector> schema_vector; + ParquetBuilder::impl::addFieldsToSchema(schema_vector, fieldList, &batchRecType, geoData, rec_type, 0, 0); + if(geoData.as_geo) schema_vector.push_back(arrow::field("geometry", arrow::binary())); + pimpl->schema = make_shared(schema_vector); + fieldIterator = new field_iterator_t(fieldList); + + /* Row Based Parameters */ + batchRowSizeBytes = RecordObject::getRecordDataSize(batchRecType); + rowSizeBytes = RecordObject::getRecordDataSize(rec_type) + batchRowSizeBytes; + maxRowsInGroup = ROW_GROUP_SIZE / rowSizeBytes; + + /* Initialize Queues */ + int qdepth = maxRowsInGroup * QUEUE_BUFFER_FACTOR; + outQ = new Publisher(outq_name, Publisher::defaultFree, qdepth); + inQ = new Subscriber(inq_name, MsgQ::SUBSCRIBER_OF_CONFIDENCE, qdepth); + + /* Create Unique Temporary Filename */ + FString tmp_file("%s%s.parquet", TMP_FILE_PREFIX, id); + fileName = tmp_file.c_str(true); + + /* Create Arrow Output Stream */ + shared_ptr file_output_stream; + PARQUET_ASSIGN_OR_THROW(file_output_stream, arrow::io::FileOutputStream::Open(fileName)); + + /* Create Writer Properties */ + parquet::WriterProperties::Builder writer_props_builder; + writer_props_builder.compression(parquet::Compression::SNAPPY); + writer_props_builder.version(parquet::ParquetVersion::PARQUET_2_6); + shared_ptr writer_props = writer_props_builder.build(); + + /* Create Arrow Writer Properties */ + auto arrow_writer_props = parquet::ArrowWriterProperties::Builder().store_schema()->build(); + + /* Build GeoParquet MetaData */ + auto metadata = pimpl->schema->metadata() ? pimpl->schema->metadata()->Copy() : std::make_shared(); + if(geoData.as_geo) ParquetBuilder::impl::appendGeoMetaData(metadata); + ParquetBuilder::impl::appendServerMetaData(metadata); + ParquetBuilder::impl::appendPandasMetaData(metadata, pimpl->schema, fieldIterator, index_key, geoData.as_geo); + pimpl->schema = pimpl->schema->WithMetadata(metadata); + + /* Create Parquet Writer */ + #ifdef APACHE_ARROW_10_COMPAT + (void)parquet::arrow::FileWriter::Open(*pimpl->schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props, &pimpl->parquetWriter); + #elif 0 // alternative method of creating file writer + std::shared_ptr parquet_schema; + (void)parquet::arrow::ToParquetSchema(pimpl->schema.get(), *writer_props, *arrow_writer_props, &parquet_schema); + auto schema_node = std::static_pointer_cast(parquet_schema->schema_root()); + std::unique_ptr base_writer; + base_writer = parquet::ParquetFileWriter::Open(std::move(file_output_stream), schema_node, std::move(writer_props), metadata); + auto schema_ptr = std::make_shared<::arrow::Schema>(*pimpl->schema); + (void)parquet::arrow::FileWriter::Make(::arrow::default_memory_pool(), std::move(base_writer), std::move(schema_ptr), std::move(arrow_writer_props), &pimpl->parquetWriter); + #else + arrow::Result> result = parquet::arrow::FileWriter::Open(*pimpl->schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props); + if(result.ok()) pimpl->parquetWriter = std::move(result).ValueOrDie(); + else mlog(CRITICAL, "Failed to open parquet writer: %s", result.status().ToString().c_str()); + #endif + + /* Start Builder Thread */ + active = true; + builderPid = new Thread(builderThread, this); +} + +/*---------------------------------------------------------------------------- + * Destructor - + *----------------------------------------------------------------------------*/ +ParquetBuilder::~ParquetBuilder(void) +{ + active = false; + delete builderPid; + parms->releaseLuaObject(); + delete [] fileName; + delete [] outputPath; + delete [] recType; + delete outQ; + delete inQ; + delete fieldIterator; + delete pimpl; + if(geoData.as_geo) + { + delete [] geoData.x_key; + delete [] geoData.y_key; + } +} + +/*---------------------------------------------------------------------------- + * builderThread + *----------------------------------------------------------------------------*/ +void* ParquetBuilder::builderThread(void* parm) +{ + ParquetBuilder* builder = static_cast(parm); + int row_cnt = 0; - case RecordObject::UINT16: - { - arrow::UInt16Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((uint16_t)batch.record->getValueInteger(field)); - field.offset += batchRowSizeBytes * 8; - } - field.offset = starting_offset; - } - else // non-batch field - { - uint16_t value = (uint16_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = recordBatch.next(&batch); - } - (void)builder.Finish(&column); - break; - } + /* Early Exit on No Writer */ + if(!builder->pimpl->parquetWriter) + { + return NULL; + } - case RecordObject::UINT32: - { - arrow::UInt32Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((uint32_t)batch.record->getValueInteger(field)); - field.offset += batchRowSizeBytes * 8; - } - field.offset = starting_offset; - } - else // non-batch field - { - uint32_t value = (uint32_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = recordBatch.next(&batch); - } - (void)builder.Finish(&column); - break; - } + /* Start Trace */ + uint32_t trace_id = start_trace(INFO, builder->traceId, "parquet_builder", "{\"filename\":\"%s\"}", builder->fileName); + EventLib::stashId(trace_id); - case RecordObject::UINT64: + /* Loop Forever */ + while(builder->active) + { + /* Receive Message */ + Subscriber::msgRef_t ref; + int recv_status = builder->inQ->receiveRef(ref, SYS_TIMEOUT); + if(recv_status > 0) + { + /* Process Record */ + if(ref.size > 0) { - arrow::UInt64Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + /* Get Record and Match to Type being Processed */ + RecordInterface* record = new RecordInterface((unsigned char*)ref.data, ref.size); + if(!StringLib::match(record->getRecordType(), builder->recType)) { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((uint64_t)batch.record->getValueInteger(field)); - field.offset += batchRowSizeBytes * 8; - } - field.offset = starting_offset; - } - else // non-batch field - { - uint64_t value = (uint64_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = recordBatch.next(&batch); + delete record; + builder->outQ->postCopy(ref.data, ref.size); + builder->inQ->dereference(ref); + continue; } - (void)builder.Finish(&column); - break; - } - case RecordObject::TIME8: - { - arrow::TimestampBuilder builder(arrow::timestamp(arrow::TimeUnit::NANO), arrow::default_memory_pool()); - (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + /* Determine Rows in Record */ + int record_size_bytes = record->getAllocatedDataSize(); + int batch_size_bytes = record_size_bytes - (builder->rowSizeBytes - builder->batchRowSizeBytes); + int num_rows = batch_size_bytes / builder->batchRowSizeBytes; + int left_over = batch_size_bytes % builder->batchRowSizeBytes; + if(left_over > 0) { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((int64_t)batch.record->getValueInteger(field)); - field.offset += batchRowSizeBytes * 8; - } - field.offset = starting_offset; - } - else // non-batch field - { - int64_t value = (int64_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = recordBatch.next(&batch); + mlog(ERROR, "Invalid record size received for %s: %d %% %d != 0", record->getRecordType(), batch_size_bytes, builder->batchRowSizeBytes); + delete record; // record is not batched, so must delete here + builder->inQ->dereference(ref); // record is not batched, so must dereference here + continue; } - (void)builder.Finish(&column); - break; - } - case RecordObject::STRING: - { - arrow::StringBuilder builder; - (void)builder.Reserve(num_rows); - unsigned long key = recordBatch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + /* Create Batch Structure */ + batch_t batch = { + .ref = ref, + .record = record, + .rows = num_rows + }; + + /* Add Batch to Ordering */ + builder->recordBatch.add(row_cnt, batch); + row_cnt += num_rows; + if(row_cnt >= builder->maxRowsInGroup) { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - const char* str = batch.record->getValueText(field); - builder.UnsafeAppend(str, StringLib::size(str)); - field.offset += batchRowSizeBytes * 8; - } - field.offset = starting_offset; - } - else // non-batch field - { - const char* str = batch.record->getValueText(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(str, StringLib::size(str)); - } - } - key = recordBatch.next(&batch); + builder->processRecordBatch(row_cnt); + row_cnt = 0; } - (void)builder.Finish(&column); - break; } - - default: + else { - break; + /* Terminating Message */ + mlog(DEBUG, "Terminator received on %s, exiting parquet builder", builder->inQ->getName()); + builder->active = false; // breaks out of loop + builder->inQ->dereference(ref); // terminator is not batched, so must dereference here } } + else if(recv_status != MsgQ::STATE_TIMEOUT) + { + /* Break Out on Failure */ + mlog(CRITICAL, "Failed queue receive on %s with error %d", builder->inQ->getName(), recv_status); + builder->active = false; // breaks out of loop + } + } + + /* Process Remaining Records */ + builder->processRecordBatch(row_cnt); + + /* Close Parquet Writer */ + (void)builder->pimpl->parquetWriter->Close(); + + /* Send File to User */ + const char* _path = builder->outputPath; + uint32_t send_trace_id = start_trace(INFO, trace_id, "send_file", "{\"path\": \"%s\"}", _path); + int _path_len = StringLib::size(_path); + if( (_path_len > 5) && + (_path[0] == 's') && + (_path[1] == '3') && + (_path[2] == ':') && + (_path[3] == '/') && + (_path[4] == '/')) + { + /* Upload File to S3 */ + builder->send2S3(&_path[5]); + } + else + { + /* Stream File Back to Client */ + builder->send2Client(); + } + + /* Remove File */ + int rc = remove(builder->fileName); + if(rc != 0) + { + mlog(CRITICAL, "Failed (%d) to delete file %s: %s", rc, builder->fileName, strerror(errno)); + } + + stop_trace(INFO, send_trace_id); + + /* Signal Completion */ + builder->signalComplete(); + + /* Stop Trace */ + stop_trace(INFO, trace_id); + + /* Exit Thread */ + return NULL; +} + +/*---------------------------------------------------------------------------- + * processRecordBatch + *----------------------------------------------------------------------------*/ +void ParquetBuilder::processRecordBatch (int num_rows) +{ + batch_t batch; + + /* Start Trace */ + uint32_t parent_trace_id = EventLib::grabId(); + uint32_t trace_id = start_trace(INFO, parent_trace_id, "process_batch", "{\"num_rows\": %d}", num_rows); + + /* Loop Through Fields in Schema */ + vector> columns; + for(int i = 0; i < fieldIterator->length; i++) + { + uint32_t field_trace_id = start_trace(INFO, trace_id, "append_field", "{\"field\": %d}", i); + RecordObject::field_t field = (*fieldIterator)[i]; + + /* Build Column */ + shared_ptr column; + if(field.elements <= 1) pimpl->processField(field, &column, recordBatch, num_rows, batchRowSizeBytes * 8); + else pimpl->processArray(field, &column, recordBatch, batchRowSizeBytes * 8); /* Add Column to Columns */ columns.push_back(column); @@ -1222,7 +1544,11 @@ void ParquetBuilder::processRecordBatch (int num_rows) if(pimpl->parquetWriter) { shared_ptr table = arrow::Table::Make(pimpl->schema, columns); - (void)pimpl->parquetWriter->WriteTable(*table, num_rows); + arrow::Status s = pimpl->parquetWriter->WriteTable(*table, num_rows); + if(!s.ok()) + { + alert(RTE_ERROR, CRITICAL, outQ, NULL, "Failed to write parquet table: %s", s.CodeAsString().c_str()); + } } stop_trace(INFO, write_trace_id); @@ -1330,6 +1656,9 @@ bool ParquetBuilder::send2Client (void) long file_size = ftell(fp); fseek(fp, 0L, SEEK_SET); + /* Log Status */ + mlog(INFO, "Writing parquet file %s of size %ld", fileName, file_size); + do { /* Send Meta Record */ From 58ed25ee840c73f182971e94f6aeb3917872c341 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Mon, 26 Feb 2024 21:36:16 +0000 Subject: [PATCH 21/43] apache arrow implementation moved into its own module --- packages/arrow/ArrowImpl.cpp | 1257 +++++++++++++++++++++++++++++ packages/arrow/ArrowImpl.h | 148 ++++ packages/arrow/CMakeLists.txt | 2 + packages/arrow/ParquetBuilder.cpp | 1222 +--------------------------- packages/arrow/ParquetBuilder.h | 57 +- 5 files changed, 1458 insertions(+), 1228 deletions(-) create mode 100644 packages/arrow/ArrowImpl.cpp create mode 100644 packages/arrow/ArrowImpl.h diff --git a/packages/arrow/ArrowImpl.cpp b/packages/arrow/ArrowImpl.cpp new file mode 100644 index 000000000..0b100ea4f --- /dev/null +++ b/packages/arrow/ArrowImpl.cpp @@ -0,0 +1,1257 @@ +/* + * Copyright (c) 2021, University of Washington + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the University of Washington nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF WASHINGTON AND CONTRIBUTORS + * “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF WASHINGTON OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/****************************************************************************** + * INCLUDES + ******************************************************************************/ + +#include "core.h" +#include "ParquetBuilder.h" +#include "ArrowParms.h" +#include "ArrowImpl.h" + +#ifdef __aws__ +#include "aws.h" +#endif + +/****************************************************************************** + * PUBLIC METHODS + ******************************************************************************/ + +/*---------------------------------------------------------------------------- + * Constructor + *----------------------------------------------------------------------------*/ +ArrowImpl::ArrowImpl (ParquetBuilder::field_list_t& field_list, + const ParquetBuilder::geo_data_t& geo_data, + const char* rec_type, + const char* index_key, + const char* file_name) +{ + /* Initialize Data */ + batchRecType = NULL; + + /* Define Table Schema */ + vector> schema_vector; + addFieldsToSchema(schema_vector, field_list, &batchRecType, geo_data, rec_type, 0, 0); + if(geo_data.as_geo) schema_vector.push_back(arrow::field("geometry", arrow::binary())); + schema = make_shared(schema_vector); + fieldIterator = new field_iterator_t(field_list); + + /* Create Arrow Output Stream */ + shared_ptr file_output_stream; + PARQUET_ASSIGN_OR_THROW(file_output_stream, arrow::io::FileOutputStream::Open(file_name)); + + /* Create Writer Properties */ + parquet::WriterProperties::Builder writer_props_builder; + writer_props_builder.compression(parquet::Compression::SNAPPY); + writer_props_builder.version(parquet::ParquetVersion::PARQUET_2_6); + shared_ptr writer_props = writer_props_builder.build(); + + /* Create Arrow Writer Properties */ + auto arrow_writer_props = parquet::ArrowWriterProperties::Builder().store_schema()->build(); + + /* Build GeoParquet MetaData */ + auto metadata = schema->metadata() ? schema->metadata()->Copy() : std::make_shared(); + if(geo_data.as_geo) appendGeoMetaData(metadata); + appendServerMetaData(metadata); + appendPandasMetaData(metadata, schema, fieldIterator, index_key, geo_data.as_geo); + schema = schema->WithMetadata(metadata); + + /* Create Parquet Writer */ + #ifdef APACHE_ARROW_10_COMPAT + (void)parquet::arrow::FileWriter::Open(*schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props, &parquetWriter); + #elif 0 // alternative method of creating file writer + std::shared_ptr parquet_schema; + (void)parquet::arrow::ToParquetSchema(schema.get(), *writer_props, *arrow_writer_props, &parquet_schema); + auto schema_node = std::static_pointer_cast(parquet_schema->schema_root()); + std::unique_ptr base_writer; + base_writer = parquet::ParquetFileWriter::Open(std::move(file_output_stream), schema_node, std::move(writer_props), metadata); + auto schema_ptr = std::make_shared<::arrow::Schema>(*schema); + (void)parquet::arrow::FileWriter::Make(::arrow::default_memory_pool(), std::move(base_writer), std::move(schema_ptr), std::move(arrow_writer_props), &parquetWriter); + #else + arrow::Result> result = parquet::arrow::FileWriter::Open(*schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props); + if(result.ok()) parquetWriter = std::move(result).ValueOrDie(); + else mlog(CRITICAL, "Failed to open parquet writer: %s", result.status().ToString().c_str()); + #endif +} + +/*---------------------------------------------------------------------------- + * Destructor + *----------------------------------------------------------------------------*/ +ArrowImpl::~ArrowImpl (void) +{ + delete fieldIterator; + delete [] batchRecType; +} + +/*---------------------------------------------------------------------------- + * isValid + *----------------------------------------------------------------------------*/ +bool ArrowImpl::isValid (void) +{ + return parquetWriter != NULL; +} + +/*---------------------------------------------------------------------------- +* getBatchRecType +*----------------------------------------------------------------------------*/ +const char* ArrowImpl::getBatchRecType (void) +{ + return batchRecType; +} + +/*---------------------------------------------------------------------------- + * processRecordBatch + *----------------------------------------------------------------------------*/ +bool ArrowImpl::processRecordBatch (Ordering& record_batch, int num_rows, int batch_row_size_bits, ParquetBuilder::geo_data_t& geo_data, bool file_finished) +{ + bool status = false; + + /* Start Trace */ + uint32_t parent_trace_id = EventLib::grabId(); + uint32_t trace_id = start_trace(INFO, parent_trace_id, "process_batch", "{\"num_rows\": %d}", num_rows); + + /* Loop Through Fields in Schema */ + vector> columns; + for(int i = 0; i < fieldIterator->length; i++) + { + uint32_t field_trace_id = start_trace(INFO, trace_id, "append_field", "{\"field\": %d}", i); + RecordObject::field_t field = (*fieldIterator)[i]; + + /* Build Column */ + shared_ptr column; + if(field.elements <= 1) processField(field, &column, record_batch, num_rows, batch_row_size_bits); + else processArray(field, &column, record_batch, batch_row_size_bits); + + /* Add Column to Columns */ + columns.push_back(column); + stop_trace(INFO, field_trace_id); + } + + /* Add Geometry Column (if GeoParquet) */ + if(geo_data.as_geo) + { + uint32_t geo_trace_id = start_trace(INFO, trace_id, "geo_column", "%s", "{}"); + shared_ptr column; + processGeometry(geo_data.x_field, geo_data.y_field, &column, record_batch, num_rows, batch_row_size_bits); + columns.push_back(column); + stop_trace(INFO, geo_trace_id); + } + + /* Build and Write Table */ + uint32_t write_trace_id = start_trace(INFO, trace_id, "write_table", "%s", "{}"); + if(parquetWriter) + { + shared_ptr table = arrow::Table::Make(schema, columns); + arrow::Status s = parquetWriter->WriteTable(*table, num_rows); + if(s.ok()) status = true; + else mlog(CRITICAL, "Failed to write parquet table: %s", s.CodeAsString().c_str()); + } + stop_trace(INFO, write_trace_id); + + /* Close Parquet Writer */ + if(file_finished) + { + (void)parquetWriter->Close(); + } + + /* Stop Trace */ + stop_trace(INFO, trace_id); + + /* Return Status */ + return status; +} + +/****************************************************************************** + * PRIVATE METHODS + ******************************************************************************/ + +/*---------------------------------------------------------------------------- +* addFieldsToSchema +*----------------------------------------------------------------------------*/ +bool ArrowImpl::addFieldsToSchema ( vector>& schema_vector, + ParquetBuilder::field_list_t& field_list, + const char** batch_rec_type, + const ParquetBuilder::geo_data_t& geo_data, + const char* rec_type, + int offset, + int flags ) +{ + /* Loop Through Fields in Record */ + Dictionary* fields = RecordObject::getRecordFields(rec_type); + Dictionary::Iterator field_iter(*fields); + for(int i = 0; i < field_iter.length; i++) + { + Dictionary::kv_t kv = field_iter[i]; + const char* field_name = kv.key; + const RecordObject::field_t& field = kv.value; + bool add_field_to_list = true; + + /* Check for Geometry Columns */ + if(geo_data.as_geo) + { + if(field.offset == geo_data.x_field.offset || field.offset == geo_data.y_field.offset) + { + /* skip over source columns for geometry as they will be added + * separately as a part of the dedicated geometry column */ + continue; + } + } + + /* Check for Batch Record Type */ + if((*batch_rec_type == NULL) && (field.flags & RecordObject::BATCH)) + { + *batch_rec_type = StringLib::duplicate(field.exttype); + } + + /* Add to Schema */ + if(field.elements == 1 || field.type == RecordObject::USER) + { + switch(field.type) + { + case RecordObject::INT8: schema_vector.push_back(arrow::field(field_name, arrow::int8())); break; + case RecordObject::INT16: schema_vector.push_back(arrow::field(field_name, arrow::int16())); break; + case RecordObject::INT32: schema_vector.push_back(arrow::field(field_name, arrow::int32())); break; + case RecordObject::INT64: schema_vector.push_back(arrow::field(field_name, arrow::int64())); break; + case RecordObject::UINT8: schema_vector.push_back(arrow::field(field_name, arrow::uint8())); break; + case RecordObject::UINT16: schema_vector.push_back(arrow::field(field_name, arrow::uint16())); break; + case RecordObject::UINT32: schema_vector.push_back(arrow::field(field_name, arrow::uint32())); break; + case RecordObject::UINT64: schema_vector.push_back(arrow::field(field_name, arrow::uint64())); break; + case RecordObject::FLOAT: schema_vector.push_back(arrow::field(field_name, arrow::float32())); break; + case RecordObject::DOUBLE: schema_vector.push_back(arrow::field(field_name, arrow::float64())); break; + case RecordObject::TIME8: schema_vector.push_back(arrow::field(field_name, arrow::timestamp(arrow::TimeUnit::NANO))); break; + case RecordObject::STRING: schema_vector.push_back(arrow::field(field_name, arrow::utf8())); break; + + case RecordObject::USER: addFieldsToSchema(schema_vector, field_list, batch_rec_type, geo_data, field.exttype, field.offset, field.flags); + add_field_to_list = false; + break; + + default: add_field_to_list = false; + break; + } + } + else // array + { + switch(field.type) + { + case RecordObject::INT8: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int8()))); break; + case RecordObject::INT16: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int16()))); break; + case RecordObject::INT32: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int32()))); break; + case RecordObject::INT64: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int64()))); break; + case RecordObject::UINT8: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint8()))); break; + case RecordObject::UINT16: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint16()))); break; + case RecordObject::UINT32: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint32()))); break; + case RecordObject::UINT64: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint64()))); break; + case RecordObject::FLOAT: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::float32()))); break; + case RecordObject::DOUBLE: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::float64()))); break; + case RecordObject::TIME8: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::timestamp(arrow::TimeUnit::NANO)))); break; + case RecordObject::STRING: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::utf8()))); break; + + case RecordObject::USER: // arrays of user data types (i.e. nested structures) are not supported + add_field_to_list = false; + break; + + default: add_field_to_list = false; + break; + } + } + + /* Add to Field List */ + if(add_field_to_list) + { + RecordObject::field_t column_field = field; + column_field.offset += offset; + column_field.flags |= flags; + field_list.add(column_field); + } + } + + /* Return Success */ + return true; +} + +/*---------------------------------------------------------------------------- +* appendGeoMetaData +*----------------------------------------------------------------------------*/ +void ArrowImpl::appendGeoMetaData (const std::shared_ptr& metadata) +{ + /* Initialize Meta Data String */ + string geostr(R"json({ + "version": "1.0.0-beta.1", + "primary_column": "geometry", + "columns": { + "geometry": { + "encoding": "WKB", + "geometry_types": ["Point"], + "crs": { + "$schema": "https://proj.org/schemas/v0.5/projjson.schema.json", + "type": "GeographicCRS", + "name": "WGS 84 longitude-latitude", + "datum": { + "type": "GeodeticReferenceFrame", + "name": "World Geodetic System 1984", + "ellipsoid": { + "name": "WGS 84", + "semi_major_axis": 6378137, + "inverse_flattening": 298.257223563 + } + }, + "coordinate_system": { + "subtype": "ellipsoidal", + "axis": [ + { + "name": "Geodetic longitude", + "abbreviation": "Lon", + "direction": "east", + "unit": "degree" + }, + { + "name": "Geodetic latitude", + "abbreviation": "Lat", + "direction": "north", + "unit": "degree" + } + ] + }, + "id": { + "authority": "OGC", + "code": "CRS84" + } + }, + "edges": "planar", + "bbox": [-180.0, -90.0, 180.0, 90.0], + "epoch": 2018.0 + } + } + })json"); + + /* Reformat JSON */ + geostr = std::regex_replace(geostr, std::regex(" "), ""); + geostr = std::regex_replace(geostr, std::regex("\n"), " "); + + /* Append Meta String */ + metadata->Append("geo", geostr.c_str()); +} + +/*---------------------------------------------------------------------------- +* appendServerMetaData +*----------------------------------------------------------------------------*/ +void ArrowImpl::appendServerMetaData (const std::shared_ptr& metadata) +{ + /* Build Launch Time String */ + int64_t launch_time_gps = TimeLib::sys2gpstime(OsApi::getLaunchTime()); + TimeLib::gmt_time_t timeinfo = TimeLib::gps2gmttime(launch_time_gps); + TimeLib::date_t dateinfo = TimeLib::gmt2date(timeinfo); + FString timestr("%04d-%02d-%02dT%02d:%02d:%02dZ", timeinfo.year, dateinfo.month, dateinfo.day, timeinfo.hour, timeinfo.minute, timeinfo.second); + + /* Build Duration String */ + int64_t duration = TimeLib::gpstime() - launch_time_gps; + FString durationstr("%ld", duration); + + /* Build Package String */ + const char** pkg_list = LuaEngine::getPkgList(); + string packagestr("["); + if(pkg_list) + { + int index = 0; + while(pkg_list[index]) + { + packagestr += pkg_list[index]; + if(pkg_list[index + 1]) packagestr += ", "; + delete [] pkg_list[index]; + index++; + } + } + packagestr += "]"; + delete [] pkg_list; + + /* Initialize Meta Data String */ + string metastr(R"json({ + "server": + { + "environment":"_1_", + "version":"_2_", + "duration":_3_, + "packages":_4_, + "commit":"_5_", + "launch":"_6_" + } + })json"); + + /* Fill In Meta Data String */ + metastr = std::regex_replace(metastr, std::regex(" "), ""); + metastr = std::regex_replace(metastr, std::regex("\n"), " "); + metastr = std::regex_replace(metastr, std::regex("_1_"), OsApi::getEnvVersion()); + metastr = std::regex_replace(metastr, std::regex("_2_"), LIBID); + metastr = std::regex_replace(metastr, std::regex("_3_"), durationstr.c_str()); + metastr = std::regex_replace(metastr, std::regex("_4_"), packagestr.c_str()); + metastr = std::regex_replace(metastr, std::regex("_5_"), BUILDINFO); + metastr = std::regex_replace(metastr, std::regex("_6_"), timestr.c_str()); + + /* Append Meta String */ + metadata->Append("sliderule", metastr.c_str()); +} + +/*---------------------------------------------------------------------------- +* appendPandasMetaData +*----------------------------------------------------------------------------*/ +void ArrowImpl::appendPandasMetaData ( const std::shared_ptr& metadata, + const shared_ptr& _schema, + const field_iterator_t* field_iterator, + const char* index_key, + bool as_geo ) +{ + /* Initialize Pandas Meta Data String */ + string pandasstr(R"json({ + "index_columns": [_INDEX_], + "column_indexes": [ + { + "name": null, + "field_name": null, + "pandas_type": "unicode", + "numpy_type": "object", + "metadata": {"encoding": "UTF-8"} + } + ], + "columns": [_COLUMNS_], + "creator": {"library": "pyarrow", "version": "10.0.1"}, + "pandas_version": "1.5.3" + })json"); + + /* Build Columns String */ + string columns; + int index = 0; + for(const std::string& field_name: _schema->field_names()) + { + /* Initialize Column String */ + string columnstr(R"json({"name": "_NAME_", "field_name": "_NAME_", "pandas_type": "_PTYPE_", "numpy_type": "_NTYPE_", "metadata": null})json"); + const char* pandas_type = ""; + const char* numpy_type = ""; + bool is_last_entry = false; + + if(index < field_iterator->length) + { + /* Add Column from Field List */ + RecordObject::field_t field = (*field_iterator)[index++]; + switch(field.type) + { + case RecordObject::DOUBLE: pandas_type = "float64"; numpy_type = "float64"; break; + case RecordObject::FLOAT: pandas_type = "float32"; numpy_type = "float32"; break; + case RecordObject::INT8: pandas_type = "int8"; numpy_type = "int8"; break; + case RecordObject::INT16: pandas_type = "int16"; numpy_type = "int16"; break; + case RecordObject::INT32: pandas_type = "int32"; numpy_type = "int32"; break; + case RecordObject::INT64: pandas_type = "int64"; numpy_type = "int64"; break; + case RecordObject::UINT8: pandas_type = "uint8"; numpy_type = "uint8"; break; + case RecordObject::UINT16: pandas_type = "uint16"; numpy_type = "uint16"; break; + case RecordObject::UINT32: pandas_type = "uint32"; numpy_type = "uint32"; break; + case RecordObject::UINT64: pandas_type = "uint64"; numpy_type = "uint64"; break; + case RecordObject::TIME8: pandas_type = "datetime"; numpy_type = "datetime64[ns]"; break; + case RecordObject::STRING: pandas_type = "bytes"; numpy_type = "object"; break; + default: pandas_type = "bytes"; numpy_type = "object"; break; + } + + /* Mark Last Column */ + if(!as_geo && (index == field_iterator->length)) + { + is_last_entry = true; + } + } + else if(as_geo && StringLib::match(field_name.c_str(), "geometry")) + { + /* Add Column for Geometry */ + pandas_type = "bytes"; + numpy_type = "object"; + is_last_entry = true; + } + + /* Fill In Column String */ + columnstr = std::regex_replace(columnstr, std::regex("_NAME_"), field_name.c_str()); + columnstr = std::regex_replace(columnstr, std::regex("_PTYPE_"), pandas_type); + columnstr = std::regex_replace(columnstr, std::regex("_NTYPE_"), numpy_type); + + /* Add Comma */ + if(!is_last_entry) + { + columnstr += ", "; + } + + /* Add Column String to Columns */ + columns += columnstr; + } + + /* Build Index String */ + FString indexstr("\"%s\"", index_key ? index_key : ""); + + /* Fill In Pandas Meta Data String */ + pandasstr = std::regex_replace(pandasstr, std::regex(" "), ""); + pandasstr = std::regex_replace(pandasstr, std::regex("\n"), " "); + pandasstr = std::regex_replace(pandasstr, std::regex("_INDEX_"), index_key ? indexstr.c_str() : ""); + pandasstr = std::regex_replace(pandasstr, std::regex("_COLUMNS_"), columns.c_str()); + + /* Append Meta String */ + metadata->Append("pandas", pandasstr.c_str()); +} + +/*---------------------------------------------------------------------------- +* processField +*----------------------------------------------------------------------------*/ +void ArrowImpl::processField (RecordObject::field_t& field, shared_ptr* column, Ordering& record_batch, int num_rows, int batch_row_size_bits) +{ + ParquetBuilder::batch_t batch; + + switch(field.type) + { + case RecordObject::DOUBLE: + { + arrow::DoubleBuilder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((double)batch.record->getValueReal(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + float value = (float)batch.record->getValueReal(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::FLOAT: + { + arrow::FloatBuilder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((float)batch.record->getValueReal(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + float value = (float)batch.record->getValueReal(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::INT8: + { + arrow::Int8Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((int8_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + int8_t value = (int8_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::INT16: + { + arrow::Int16Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((int16_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + int16_t value = (int16_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::INT32: + { + arrow::Int32Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((int32_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + int32_t value = (int32_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::INT64: + { + arrow::Int64Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((int64_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + int64_t value = (int64_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::UINT8: + { + arrow::UInt8Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((uint8_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + uint8_t value = (uint8_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::UINT16: + { + arrow::UInt16Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((uint16_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + uint16_t value = (uint16_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::UINT32: + { + arrow::UInt32Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((uint32_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + uint32_t value = (uint32_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::UINT64: + { + arrow::UInt64Builder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((uint64_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + uint64_t value = (uint64_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::TIME8: + { + arrow::TimestampBuilder builder(arrow::timestamp(arrow::TimeUnit::NANO), arrow::default_memory_pool()); + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend((int64_t)batch.record->getValueInteger(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + int64_t value = (int64_t)batch.record->getValueInteger(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(value); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + case RecordObject::STRING: + { + arrow::StringBuilder builder; + (void)builder.Reserve(num_rows); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + const char* str = batch.record->getValueText(field); + builder.UnsafeAppend(str, StringLib::size(str)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + const char* str = batch.record->getValueText(field); + for(int row = 0; row < batch.rows; row++) + { + builder.UnsafeAppend(str, StringLib::size(str)); + } + } + key = record_batch.next(&batch); + } + (void)builder.Finish(column); + break; + } + + default: + { + break; + } + } +} + +/*---------------------------------------------------------------------------- +* processArray +*----------------------------------------------------------------------------*/ +void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr* column, Ordering& record_batch, int batch_row_size_bits) +{ + ParquetBuilder::batch_t batch; + + if(!(field.flags & RecordObject::BATCH)) + { + batch_row_size_bits = 0; + } + + switch(field.type) + { + case RecordObject::DOUBLE: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((double)batch.record->getValueReal(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::FLOAT: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((float)batch.record->getValueReal(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::INT8: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((int8_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::INT16: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((int16_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::INT32: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((int32_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::INT64: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((int64_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::UINT8: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((uint8_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::UINT16: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((uint16_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::UINT32: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((uint32_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::UINT64: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((uint64_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::TIME8: + { + auto builder = make_shared(arrow::timestamp(arrow::TimeUnit::NANO), arrow::default_memory_pool()); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + (void)builder->Append((int64_t)batch.record->getValueInteger(field, element)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + case RecordObject::STRING: + { + auto builder = make_shared(); + arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch.rows; row++) + { + (void)list_builder.Append(); + for(int element = 0; element < field.elements; element++) + { + const char* str = batch.record->getValueText(field, NULL, element); + (void)builder->Append(str, StringLib::size(str)); + } + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + key = record_batch.next(&batch); + } + (void)list_builder.Finish(column); + break; + } + + default: + { + break; + } + } +} + +/*---------------------------------------------------------------------------- +* processGeometry +*----------------------------------------------------------------------------*/ +void ArrowImpl::processGeometry (RecordObject::field_t& x_field, RecordObject::field_t& y_field, shared_ptr* column, Ordering& record_batch, int num_rows, int batch_row_size_bits) +{ + ParquetBuilder::batch_t batch; + arrow::BinaryBuilder builder; + (void)builder.Reserve(num_rows); + (void)builder.ReserveData(num_rows * sizeof(wkbpoint_t)); + unsigned long key = record_batch.first(&batch); + while(key != (unsigned long)INVALID_KEY) + { + int32_t starting_x_offset = x_field.offset; + int32_t starting_y_offset = y_field.offset; + for(int row = 0; row < batch.rows; row++) + { + wkbpoint_t point = { + #ifdef __be__ + .byteOrder = 0, + #else + .byteOrder = 1, + #endif + .wkbType = 1, + .x = batch.record->getValueReal(x_field), + .y = batch.record->getValueReal(y_field) + }; + (void)builder.UnsafeAppend((uint8_t*)&point, sizeof(wkbpoint_t)); + if(x_field.flags & RecordObject::BATCH) x_field.offset += batch_row_size_bits; + if(y_field.flags & RecordObject::BATCH) y_field.offset += batch_row_size_bits; + } + x_field.offset = starting_x_offset; + y_field.offset = starting_y_offset; + key = record_batch.next(&batch); + } + (void)builder.Finish(column); +} \ No newline at end of file diff --git a/packages/arrow/ArrowImpl.h b/packages/arrow/ArrowImpl.h new file mode 100644 index 000000000..72fe46078 --- /dev/null +++ b/packages/arrow/ArrowImpl.h @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2021, University of Washington + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the University of Washington nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE UNIVERSITY OF WASHINGTON AND CONTRIBUTORS + * “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE UNIVERSITY OF WASHINGTON OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef __arrow_impl__ +#define __arrow_impl__ + +/****************************************************************************** + * INCLUDES + ******************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MsgQ.h" +#include "LuaObject.h" +#include "Ordering.h" +#include "RecordObject.h" +#include "ArrowParms.h" +#include "ParquetBuilder.h" +#include "OsApi.h" +#include "MsgQ.h" + +/****************************************************************************** + * PARQUET BUILDER CLASS + ******************************************************************************/ + +class ArrowImpl +{ + public: + + /*-------------------------------------------------------------------- + * Methods + *--------------------------------------------------------------------*/ + + ArrowImpl (ParquetBuilder::field_list_t& field_list, + const ParquetBuilder::geo_data_t& geo_data, + const char* rec_type, + const char* index_key, + const char* file_name); + ~ArrowImpl (void); + + bool isValid (void); + const char* getBatchRecType (void); + bool processRecordBatch (Ordering& record_batch, + int num_rows, + int batch_row_size_bits, + ParquetBuilder::geo_data_t& geo_data, + bool file_finished=false); + + private: + + /*-------------------------------------------------------------------- + * Types + *--------------------------------------------------------------------*/ + + typedef ParquetBuilder::field_list_t::Iterator field_iterator_t; + + typedef struct WKBPoint { + uint8_t byteOrder; + uint32_t wkbType; + double x; + double y; + } ALIGN_PACKED wkbpoint_t; + + /*-------------------------------------------------------------------- + * Data + *--------------------------------------------------------------------*/ + + shared_ptr schema; + unique_ptr parquetWriter; + field_iterator_t* fieldIterator; + const char* batchRecType; + + /*-------------------------------------------------------------------- + * Methods + *--------------------------------------------------------------------*/ + + bool addFieldsToSchema (vector>& schema_vector, + ParquetBuilder::field_list_t& field_list, + const char** batch_rec_type, + const ParquetBuilder::geo_data_t& geo, + const char* rec_type, + int offset, + int flags); + void appendGeoMetaData (const std::shared_ptr& metadata); + void appendServerMetaData (const std::shared_ptr& metadata); + void appendPandasMetaData (const std::shared_ptr& metadata, + const shared_ptr& _schema, + const field_iterator_t* field_iterator, + const char* index_key, + bool as_geo); + void processField (RecordObject::field_t& field, + shared_ptr* column, + Ordering& record_batch, + int num_rows, + int batch_row_size_bits); + void processArray (RecordObject::field_t& field, + shared_ptr* column, + Ordering& record_batch, + int batch_row_size_bits); + void processGeometry (RecordObject::field_t& x_field, + RecordObject::field_t& y_field, + shared_ptr* column, + Ordering& record_batch, + int num_rows, + int batch_row_size_bits); + + + +}; + +#endif /* __arrow_impl__ */ diff --git a/packages/arrow/CMakeLists.txt b/packages/arrow/CMakeLists.txt index aa4517e40..e0a7e837f 100644 --- a/packages/arrow/CMakeLists.txt +++ b/packages/arrow/CMakeLists.txt @@ -15,6 +15,7 @@ if (Arrow_FOUND AND Parquet_FOUND) target_sources(slideruleLib PRIVATE ${CMAKE_CURRENT_LIST_DIR}/arrow.cpp + ${CMAKE_CURRENT_LIST_DIR}/ArrowImpl.cpp ${CMAKE_CURRENT_LIST_DIR}/ArrowParms.cpp ${CMAKE_CURRENT_LIST_DIR}/ParquetBuilder.cpp ) @@ -28,6 +29,7 @@ if (Arrow_FOUND AND Parquet_FOUND) install ( FILES ${CMAKE_CURRENT_LIST_DIR}/arrow.h + ${CMAKE_CURRENT_LIST_DIR}/ArrowImpl.h ${CMAKE_CURRENT_LIST_DIR}/ArrowParms.h ${CMAKE_CURRENT_LIST_DIR}/ParquetBuilder.h DESTINATION diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index fb4608dd7..e7315f64f 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -33,20 +33,10 @@ * INCLUDES ******************************************************************************/ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include "core.h" #include "ParquetBuilder.h" #include "ArrowParms.h" +#include "ArrowImpl.h" #ifdef __aws__ #include "aws.h" @@ -82,1042 +72,6 @@ const RecordObject::fieldDef_t ParquetBuilder::remoteRecDef[] = { const char* ParquetBuilder::TMP_FILE_PREFIX = "/tmp/"; -/****************************************************************************** - * PRIVATE IMPLEMENTATION - ******************************************************************************/ - -struct ParquetBuilder::impl -{ - shared_ptr schema; - unique_ptr parquetWriter; - - /*---------------------------------------------------------------------------- - * addFieldsToSchema - *----------------------------------------------------------------------------*/ - static bool addFieldsToSchema (vector>& schema_vector, - field_list_t& field_list, - const char** batch_rec_type, - const geo_data_t& geo, - const char* rec_type, - int offset, - int flags) - { - /* Loop Through Fields in Record */ - Dictionary* fields = RecordObject::getRecordFields(rec_type); - Dictionary::Iterator field_iter(*fields); - for(int i = 0; i < field_iter.length; i++) - { - Dictionary::kv_t kv = field_iter[i]; - const char* field_name = kv.key; - const RecordObject::field_t& field = kv.value; - bool add_field_to_list = true; - - /* Check for Geometry Columns */ - if(geo.as_geo) - { - if(field.offset == geo.x_field.offset || field.offset == geo.y_field.offset) - { - /* skip over source columns for geometry as they will be added - * separately as a part of the dedicated geometry column */ - continue; - } - } - - /* Check for Batch Record Type */ - if((*batch_rec_type == NULL) && (field.flags & RecordObject::BATCH)) - { - *batch_rec_type = field.exttype; - } - - /* Add to Schema */ - if(field.elements == 1 || field.type == RecordObject::USER) - { - switch(field.type) - { - case RecordObject::INT8: schema_vector.push_back(arrow::field(field_name, arrow::int8())); break; - case RecordObject::INT16: schema_vector.push_back(arrow::field(field_name, arrow::int16())); break; - case RecordObject::INT32: schema_vector.push_back(arrow::field(field_name, arrow::int32())); break; - case RecordObject::INT64: schema_vector.push_back(arrow::field(field_name, arrow::int64())); break; - case RecordObject::UINT8: schema_vector.push_back(arrow::field(field_name, arrow::uint8())); break; - case RecordObject::UINT16: schema_vector.push_back(arrow::field(field_name, arrow::uint16())); break; - case RecordObject::UINT32: schema_vector.push_back(arrow::field(field_name, arrow::uint32())); break; - case RecordObject::UINT64: schema_vector.push_back(arrow::field(field_name, arrow::uint64())); break; - case RecordObject::FLOAT: schema_vector.push_back(arrow::field(field_name, arrow::float32())); break; - case RecordObject::DOUBLE: schema_vector.push_back(arrow::field(field_name, arrow::float64())); break; - case RecordObject::TIME8: schema_vector.push_back(arrow::field(field_name, arrow::timestamp(arrow::TimeUnit::NANO))); break; - case RecordObject::STRING: schema_vector.push_back(arrow::field(field_name, arrow::utf8())); break; - - case RecordObject::USER: addFieldsToSchema(schema_vector, field_list, batch_rec_type, geo, field.exttype, field.offset, field.flags); - add_field_to_list = false; - break; - - default: add_field_to_list = false; - break; - } - } - else // array - { - switch(field.type) - { - case RecordObject::INT8: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int8()))); break; - case RecordObject::INT16: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int16()))); break; - case RecordObject::INT32: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int32()))); break; - case RecordObject::INT64: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int64()))); break; - case RecordObject::UINT8: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint8()))); break; - case RecordObject::UINT16: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint16()))); break; - case RecordObject::UINT32: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint32()))); break; - case RecordObject::UINT64: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint64()))); break; - case RecordObject::FLOAT: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::float32()))); break; - case RecordObject::DOUBLE: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::float64()))); break; - case RecordObject::TIME8: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::timestamp(arrow::TimeUnit::NANO)))); break; - case RecordObject::STRING: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::utf8()))); break; - - case RecordObject::USER: // arrays of user data types (i.e. nested structures) are not supported - add_field_to_list = false; - break; - - default: add_field_to_list = false; - break; - } - } - - /* Add to Field List */ - if(add_field_to_list) - { - RecordObject::field_t column_field = field; - column_field.offset += offset; - column_field.flags |= flags; - field_list.add(column_field); - } - } - - /* Return Success */ - return true; - } - - /*---------------------------------------------------------------------------- - * appendGeoMetaData - *----------------------------------------------------------------------------*/ - static void appendGeoMetaData (const std::shared_ptr& metadata) - { - /* Initialize Meta Data String */ - string geostr(R"json({ - "version": "1.0.0-beta.1", - "primary_column": "geometry", - "columns": { - "geometry": { - "encoding": "WKB", - "geometry_types": ["Point"], - "crs": { - "$schema": "https://proj.org/schemas/v0.5/projjson.schema.json", - "type": "GeographicCRS", - "name": "WGS 84 longitude-latitude", - "datum": { - "type": "GeodeticReferenceFrame", - "name": "World Geodetic System 1984", - "ellipsoid": { - "name": "WGS 84", - "semi_major_axis": 6378137, - "inverse_flattening": 298.257223563 - } - }, - "coordinate_system": { - "subtype": "ellipsoidal", - "axis": [ - { - "name": "Geodetic longitude", - "abbreviation": "Lon", - "direction": "east", - "unit": "degree" - }, - { - "name": "Geodetic latitude", - "abbreviation": "Lat", - "direction": "north", - "unit": "degree" - } - ] - }, - "id": { - "authority": "OGC", - "code": "CRS84" - } - }, - "edges": "planar", - "bbox": [-180.0, -90.0, 180.0, 90.0], - "epoch": 2018.0 - } - } - })json"); - - /* Reformat JSON */ - geostr = std::regex_replace(geostr, std::regex(" "), ""); - geostr = std::regex_replace(geostr, std::regex("\n"), " "); - - /* Append Meta String */ - metadata->Append("geo", geostr.c_str()); - } - - /*---------------------------------------------------------------------------- - * appendServerMetaData - *----------------------------------------------------------------------------*/ - static void appendServerMetaData (const std::shared_ptr& metadata) - { - /* Build Launch Time String */ - int64_t launch_time_gps = TimeLib::sys2gpstime(OsApi::getLaunchTime()); - TimeLib::gmt_time_t timeinfo = TimeLib::gps2gmttime(launch_time_gps); - TimeLib::date_t dateinfo = TimeLib::gmt2date(timeinfo); - FString timestr("%04d-%02d-%02dT%02d:%02d:%02dZ", timeinfo.year, dateinfo.month, dateinfo.day, timeinfo.hour, timeinfo.minute, timeinfo.second); - - /* Build Duration String */ - int64_t duration = TimeLib::gpstime() - launch_time_gps; - FString durationstr("%ld", duration); - - /* Build Package String */ - const char** pkg_list = LuaEngine::getPkgList(); - string packagestr("["); - if(pkg_list) - { - int index = 0; - while(pkg_list[index]) - { - packagestr += pkg_list[index]; - if(pkg_list[index + 1]) packagestr += ", "; - delete [] pkg_list[index]; - index++; - } - } - packagestr += "]"; - delete [] pkg_list; - - /* Initialize Meta Data String */ - string metastr(R"json({ - "server": - { - "environment":"_1_", - "version":"_2_", - "duration":_3_, - "packages":_4_, - "commit":"_5_", - "launch":"_6_" - } - })json"); - - /* Fill In Meta Data String */ - metastr = std::regex_replace(metastr, std::regex(" "), ""); - metastr = std::regex_replace(metastr, std::regex("\n"), " "); - metastr = std::regex_replace(metastr, std::regex("_1_"), OsApi::getEnvVersion()); - metastr = std::regex_replace(metastr, std::regex("_2_"), LIBID); - metastr = std::regex_replace(metastr, std::regex("_3_"), durationstr.c_str()); - metastr = std::regex_replace(metastr, std::regex("_4_"), packagestr.c_str()); - metastr = std::regex_replace(metastr, std::regex("_5_"), BUILDINFO); - metastr = std::regex_replace(metastr, std::regex("_6_"), timestr.c_str()); - - /* Append Meta String */ - metadata->Append("sliderule", metastr.c_str()); - } - - /*---------------------------------------------------------------------------- - * appendPandasMetaData - *----------------------------------------------------------------------------*/ - static void appendPandasMetaData (const std::shared_ptr& metadata, - const shared_ptr& _schema, - const field_iterator_t* field_iterator, - const char* index_key, - bool as_geo) - { - /* Initialize Pandas Meta Data String */ - string pandasstr(R"json({ - "index_columns": [_INDEX_], - "column_indexes": [ - { - "name": null, - "field_name": null, - "pandas_type": "unicode", - "numpy_type": "object", - "metadata": {"encoding": "UTF-8"} - } - ], - "columns": [_COLUMNS_], - "creator": {"library": "pyarrow", "version": "10.0.1"}, - "pandas_version": "1.5.3" - })json"); - - /* Build Columns String */ - string columns; - int index = 0; - for(const std::string& field_name: _schema->field_names()) - { - /* Initialize Column String */ - string columnstr(R"json({"name": "_NAME_", "field_name": "_NAME_", "pandas_type": "_PTYPE_", "numpy_type": "_NTYPE_", "metadata": null})json"); - const char* pandas_type = ""; - const char* numpy_type = ""; - bool is_last_entry = false; - - if(index < field_iterator->length) - { - /* Add Column from Field List */ - RecordObject::field_t field = (*field_iterator)[index++]; - switch(field.type) - { - case RecordObject::DOUBLE: pandas_type = "float64"; numpy_type = "float64"; break; - case RecordObject::FLOAT: pandas_type = "float32"; numpy_type = "float32"; break; - case RecordObject::INT8: pandas_type = "int8"; numpy_type = "int8"; break; - case RecordObject::INT16: pandas_type = "int16"; numpy_type = "int16"; break; - case RecordObject::INT32: pandas_type = "int32"; numpy_type = "int32"; break; - case RecordObject::INT64: pandas_type = "int64"; numpy_type = "int64"; break; - case RecordObject::UINT8: pandas_type = "uint8"; numpy_type = "uint8"; break; - case RecordObject::UINT16: pandas_type = "uint16"; numpy_type = "uint16"; break; - case RecordObject::UINT32: pandas_type = "uint32"; numpy_type = "uint32"; break; - case RecordObject::UINT64: pandas_type = "uint64"; numpy_type = "uint64"; break; - case RecordObject::TIME8: pandas_type = "datetime"; numpy_type = "datetime64[ns]"; break; - case RecordObject::STRING: pandas_type = "bytes"; numpy_type = "object"; break; - default: pandas_type = "bytes"; numpy_type = "object"; break; - } - - /* Mark Last Column */ - if(!as_geo && (index == field_iterator->length)) - { - is_last_entry = true; - } - } - else if(as_geo && StringLib::match(field_name.c_str(), "geometry")) - { - /* Add Column for Geometry */ - pandas_type = "bytes"; - numpy_type = "object"; - is_last_entry = true; - } - - /* Fill In Column String */ - columnstr = std::regex_replace(columnstr, std::regex("_NAME_"), field_name.c_str()); - columnstr = std::regex_replace(columnstr, std::regex("_PTYPE_"), pandas_type); - columnstr = std::regex_replace(columnstr, std::regex("_NTYPE_"), numpy_type); - - /* Add Comma */ - if(!is_last_entry) - { - columnstr += ", "; - } - - /* Add Column String to Columns */ - columns += columnstr; - } - - /* Build Index String */ - FString indexstr("\"%s\"", index_key ? index_key : ""); - - /* Fill In Pandas Meta Data String */ - pandasstr = std::regex_replace(pandasstr, std::regex(" "), ""); - pandasstr = std::regex_replace(pandasstr, std::regex("\n"), " "); - pandasstr = std::regex_replace(pandasstr, std::regex("_INDEX_"), index_key ? indexstr.c_str() : ""); - pandasstr = std::regex_replace(pandasstr, std::regex("_COLUMNS_"), columns.c_str()); - - /* Append Meta String */ - metadata->Append("pandas", pandasstr.c_str()); - } - - /*---------------------------------------------------------------------------- - * processField - *----------------------------------------------------------------------------*/ - static void processField (RecordObject::field_t& field, shared_ptr* column, Ordering& record_batch, int num_rows, int batch_row_size_bits) - { - batch_t batch; - - switch(field.type) - { - case RecordObject::DOUBLE: - { - arrow::DoubleBuilder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((double)batch.record->getValueReal(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - float value = (float)batch.record->getValueReal(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::FLOAT: - { - arrow::FloatBuilder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((float)batch.record->getValueReal(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - float value = (float)batch.record->getValueReal(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::INT8: - { - arrow::Int8Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((int8_t)batch.record->getValueInteger(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - int8_t value = (int8_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::INT16: - { - arrow::Int16Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((int16_t)batch.record->getValueInteger(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - int16_t value = (int16_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::INT32: - { - arrow::Int32Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((int32_t)batch.record->getValueInteger(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - int32_t value = (int32_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::INT64: - { - arrow::Int64Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((int64_t)batch.record->getValueInteger(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - int64_t value = (int64_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::UINT8: - { - arrow::UInt8Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((uint8_t)batch.record->getValueInteger(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - uint8_t value = (uint8_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::UINT16: - { - arrow::UInt16Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((uint16_t)batch.record->getValueInteger(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - uint16_t value = (uint16_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::UINT32: - { - arrow::UInt32Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((uint32_t)batch.record->getValueInteger(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - uint32_t value = (uint32_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::UINT64: - { - arrow::UInt64Builder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((uint64_t)batch.record->getValueInteger(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - uint64_t value = (uint64_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::TIME8: - { - arrow::TimestampBuilder builder(arrow::timestamp(arrow::TimeUnit::NANO), arrow::default_memory_pool()); - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend((int64_t)batch.record->getValueInteger(field)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - int64_t value = (int64_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(value); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - case RecordObject::STRING: - { - arrow::StringBuilder builder; - (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - if(field.flags & RecordObject::BATCH) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - const char* str = batch.record->getValueText(field); - builder.UnsafeAppend(str, StringLib::size(str)); - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - } - else // non-batch field - { - const char* str = batch.record->getValueText(field); - for(int row = 0; row < batch.rows; row++) - { - builder.UnsafeAppend(str, StringLib::size(str)); - } - } - key = record_batch.next(&batch); - } - (void)builder.Finish(column); - break; - } - - default: - { - break; - } - } - } - - /*---------------------------------------------------------------------------- - * processArray - *----------------------------------------------------------------------------*/ - static void processArray (RecordObject::field_t& field, shared_ptr* column, Ordering& record_batch, int batch_row_size_bits) - { - batch_t batch; - - if(!(field.flags & RecordObject::BATCH)) - { - batch_row_size_bits = 0; - } - - switch(field.type) - { - case RecordObject::DOUBLE: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((double)batch.record->getValueReal(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::FLOAT: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((float)batch.record->getValueReal(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::INT8: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((int8_t)batch.record->getValueInteger(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::INT16: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((int16_t)batch.record->getValueInteger(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::INT32: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((int32_t)batch.record->getValueInteger(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::INT64: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((int64_t)batch.record->getValueInteger(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::UINT8: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((uint8_t)batch.record->getValueInteger(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::UINT16: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((uint16_t)batch.record->getValueInteger(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::UINT32: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((uint32_t)batch.record->getValueInteger(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::UINT64: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((uint64_t)batch.record->getValueInteger(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::TIME8: - { - auto builder = make_shared(arrow::timestamp(arrow::TimeUnit::NANO), arrow::default_memory_pool()); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - (void)builder->Append((int64_t)batch.record->getValueInteger(field, element)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - case RecordObject::STRING: - { - auto builder = make_shared(); - arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) - { - (void)list_builder.Append(); - for(int element = 0; element < field.elements; element++) - { - const char* str = batch.record->getValueText(field, NULL, element); - (void)builder->Append(str, StringLib::size(str)); - } - field.offset += batch_row_size_bits; - } - field.offset = starting_offset; - key = record_batch.next(&batch); - } - (void)list_builder.Finish(column); - break; - } - - default: - { - break; - } - } - } - -}; - /****************************************************************************** * PUBLIC METHODS ******************************************************************************/ @@ -1212,7 +166,6 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, LuaObject(L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), parms(_parms), recType(StringLib::duplicate(rec_type)), - batchRecType(NULL), fieldList(LIST_BLOCK_SIZE), geoData(geo) { @@ -1254,19 +207,16 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, outputPath = StringLib::duplicate(parms->path); } - /* Allocate Private Implementation */ - pimpl = new ParquetBuilder::impl; + /* Create Unique Temporary Filename */ + FString tmp_file("%s%s.parquet", TMP_FILE_PREFIX, id); + fileName = tmp_file.c_str(true); - /* Define Table Schema */ - vector> schema_vector; - ParquetBuilder::impl::addFieldsToSchema(schema_vector, fieldList, &batchRecType, geoData, rec_type, 0, 0); - if(geoData.as_geo) schema_vector.push_back(arrow::field("geometry", arrow::binary())); - pimpl->schema = make_shared(schema_vector); - fieldIterator = new field_iterator_t(fieldList); + /* Allocate Implementation */ + impl = new ArrowImpl(fieldList, geoData, recType, index_key, fileName); /* Row Based Parameters */ - batchRowSizeBytes = RecordObject::getRecordDataSize(batchRecType); - rowSizeBytes = RecordObject::getRecordDataSize(rec_type) + batchRowSizeBytes; + batchRowSizeBytes = RecordObject::getRecordDataSize(impl->getBatchRecType()); + rowSizeBytes = RecordObject::getRecordDataSize(recType) + batchRowSizeBytes; maxRowsInGroup = ROW_GROUP_SIZE / rowSizeBytes; /* Initialize Queues */ @@ -1274,50 +224,17 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, outQ = new Publisher(outq_name, Publisher::defaultFree, qdepth); inQ = new Subscriber(inq_name, MsgQ::SUBSCRIBER_OF_CONFIDENCE, qdepth); - /* Create Unique Temporary Filename */ - FString tmp_file("%s%s.parquet", TMP_FILE_PREFIX, id); - fileName = tmp_file.c_str(true); - - /* Create Arrow Output Stream */ - shared_ptr file_output_stream; - PARQUET_ASSIGN_OR_THROW(file_output_stream, arrow::io::FileOutputStream::Open(fileName)); - - /* Create Writer Properties */ - parquet::WriterProperties::Builder writer_props_builder; - writer_props_builder.compression(parquet::Compression::SNAPPY); - writer_props_builder.version(parquet::ParquetVersion::PARQUET_2_6); - shared_ptr writer_props = writer_props_builder.build(); - - /* Create Arrow Writer Properties */ - auto arrow_writer_props = parquet::ArrowWriterProperties::Builder().store_schema()->build(); - - /* Build GeoParquet MetaData */ - auto metadata = pimpl->schema->metadata() ? pimpl->schema->metadata()->Copy() : std::make_shared(); - if(geoData.as_geo) ParquetBuilder::impl::appendGeoMetaData(metadata); - ParquetBuilder::impl::appendServerMetaData(metadata); - ParquetBuilder::impl::appendPandasMetaData(metadata, pimpl->schema, fieldIterator, index_key, geoData.as_geo); - pimpl->schema = pimpl->schema->WithMetadata(metadata); - - /* Create Parquet Writer */ - #ifdef APACHE_ARROW_10_COMPAT - (void)parquet::arrow::FileWriter::Open(*pimpl->schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props, &pimpl->parquetWriter); - #elif 0 // alternative method of creating file writer - std::shared_ptr parquet_schema; - (void)parquet::arrow::ToParquetSchema(pimpl->schema.get(), *writer_props, *arrow_writer_props, &parquet_schema); - auto schema_node = std::static_pointer_cast(parquet_schema->schema_root()); - std::unique_ptr base_writer; - base_writer = parquet::ParquetFileWriter::Open(std::move(file_output_stream), schema_node, std::move(writer_props), metadata); - auto schema_ptr = std::make_shared<::arrow::Schema>(*pimpl->schema); - (void)parquet::arrow::FileWriter::Make(::arrow::default_memory_pool(), std::move(base_writer), std::move(schema_ptr), std::move(arrow_writer_props), &pimpl->parquetWriter); - #else - arrow::Result> result = parquet::arrow::FileWriter::Open(*pimpl->schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props); - if(result.ok()) pimpl->parquetWriter = std::move(result).ValueOrDie(); - else mlog(CRITICAL, "Failed to open parquet writer: %s", result.status().ToString().c_str()); - #endif - /* Start Builder Thread */ - active = true; - builderPid = new Thread(builderThread, this); + if(impl->isValid()) + { + active = true; + builderPid = new Thread(builderThread, this); + } + else + { + active = false; + builderPid = NULL; + } } /*---------------------------------------------------------------------------- @@ -1333,8 +250,7 @@ ParquetBuilder::~ParquetBuilder(void) delete [] recType; delete outQ; delete inQ; - delete fieldIterator; - delete pimpl; + delete impl; if(geoData.as_geo) { delete [] geoData.x_key; @@ -1350,12 +266,6 @@ void* ParquetBuilder::builderThread(void* parm) ParquetBuilder* builder = static_cast(parm); int row_cnt = 0; - /* Early Exit on No Writer */ - if(!builder->pimpl->parquetWriter) - { - return NULL; - } - /* Start Trace */ uint32_t trace_id = start_trace(INFO, builder->traceId, "parquet_builder", "{\"filename\":\"%s\"}", builder->fileName); EventLib::stashId(trace_id); @@ -1406,7 +316,9 @@ void* ParquetBuilder::builderThread(void* parm) row_cnt += num_rows; if(row_cnt >= builder->maxRowsInGroup) { - builder->processRecordBatch(row_cnt); + bool status = builder->impl->processRecordBatch(builder->recordBatch, row_cnt, builder->batchRowSizeBytes * 8, builder->geoData); + if(!status) alert(RTE_ERROR, INFO, builder->outQ, NULL, "Failed to process record batch for %s", builder->outputPath); + builder->clearBatch(trace_id); row_cnt = 0; } } @@ -1427,10 +339,9 @@ void* ParquetBuilder::builderThread(void* parm) } /* Process Remaining Records */ - builder->processRecordBatch(row_cnt); - - /* Close Parquet Writer */ - (void)builder->pimpl->parquetWriter->Close(); + bool status = builder->impl->processRecordBatch(builder->recordBatch, row_cnt, builder->batchRowSizeBytes * 8, builder->geoData, true); + if(!status) alert(RTE_ERROR, INFO, builder->outQ, NULL, "Failed to process last record batch for %s", builder->outputPath); + builder->clearBatch(trace_id); /* Send File to User */ const char* _path = builder->outputPath; @@ -1472,87 +383,11 @@ void* ParquetBuilder::builderThread(void* parm) } /*---------------------------------------------------------------------------- - * processRecordBatch + * clearBatch *----------------------------------------------------------------------------*/ -void ParquetBuilder::processRecordBatch (int num_rows) +void ParquetBuilder::clearBatch (uint32_t trace_id) { batch_t batch; - - /* Start Trace */ - uint32_t parent_trace_id = EventLib::grabId(); - uint32_t trace_id = start_trace(INFO, parent_trace_id, "process_batch", "{\"num_rows\": %d}", num_rows); - - /* Loop Through Fields in Schema */ - vector> columns; - for(int i = 0; i < fieldIterator->length; i++) - { - uint32_t field_trace_id = start_trace(INFO, trace_id, "append_field", "{\"field\": %d}", i); - RecordObject::field_t field = (*fieldIterator)[i]; - - /* Build Column */ - shared_ptr column; - if(field.elements <= 1) pimpl->processField(field, &column, recordBatch, num_rows, batchRowSizeBytes * 8); - else pimpl->processArray(field, &column, recordBatch, batchRowSizeBytes * 8); - - /* Add Column to Columns */ - columns.push_back(column); - stop_trace(INFO, field_trace_id); - } - - /* Add Geometry Column (if GeoParquet) */ - if(geoData.as_geo) - { - uint32_t geo_trace_id = start_trace(INFO, trace_id, "geo_column", "%s", "{}"); - RecordObject::field_t x_field = geoData.x_field; - RecordObject::field_t y_field = geoData.y_field; - shared_ptr column; - arrow::BinaryBuilder builder; - (void)builder.Reserve(num_rows); - (void)builder.ReserveData(num_rows * sizeof(wkbpoint_t)); - unsigned long key = recordBatch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - int32_t starting_x_offset = x_field.offset; - int32_t starting_y_offset = y_field.offset; - for(int row = 0; row < batch.rows; row++) - { - wkbpoint_t point = { - #ifdef __be__ - .byteOrder = 0, - #else - .byteOrder = 1, - #endif - .wkbType = 1, - .x = batch.record->getValueReal(x_field), - .y = batch.record->getValueReal(y_field) - }; - (void)builder.UnsafeAppend((uint8_t*)&point, sizeof(wkbpoint_t)); - if(x_field.flags & RecordObject::BATCH) x_field.offset += batchRowSizeBytes * 8; - if(y_field.flags & RecordObject::BATCH) y_field.offset += batchRowSizeBytes * 8; - } - x_field.offset = starting_x_offset; - y_field.offset = starting_y_offset; - key = recordBatch.next(&batch); - } - (void)builder.Finish(&column); - columns.push_back(column); - stop_trace(INFO, geo_trace_id); - } - - /* Build and Write Table */ - uint32_t write_trace_id = start_trace(INFO, trace_id, "write_table", "%s", "{}"); - if(pimpl->parquetWriter) - { - shared_ptr table = arrow::Table::Make(pimpl->schema, columns); - arrow::Status s = pimpl->parquetWriter->WriteTable(*table, num_rows); - if(!s.ok()) - { - alert(RTE_ERROR, CRITICAL, outQ, NULL, "Failed to write parquet table: %s", s.CodeAsString().c_str()); - } - } - stop_trace(INFO, write_trace_id); - - /* Clear Record Batch */ uint32_t clear_trace_id = start_trace(INFO, trace_id, "clear_batch", "%s", "{}"); unsigned long key = recordBatch.first(&batch); while(key != (unsigned long)INVALID_KEY) @@ -1563,9 +398,6 @@ void ParquetBuilder::processRecordBatch (int num_rows) } recordBatch.clear(); stop_trace(INFO, clear_trace_id); - - /* Stop Trace */ - stop_trace(INFO, trace_id); } /*---------------------------------------------------------------------------- diff --git a/packages/arrow/ParquetBuilder.h b/packages/arrow/ParquetBuilder.h index f60db505e..4ccbea029 100644 --- a/packages/arrow/ParquetBuilder.h +++ b/packages/arrow/ParquetBuilder.h @@ -52,6 +52,12 @@ #include "OsApi.h" #include "MsgQ.h" +/****************************************************************************** + * FORWARD DECLARATIONS + ******************************************************************************/ + +class ArrowImpl; // arrow implementation + /****************************************************************************** * PARQUET BUILDER CLASS ******************************************************************************/ @@ -90,6 +96,22 @@ class ParquetBuilder: public LuaObject * Types *--------------------------------------------------------------------*/ + typedef List field_list_t; + + typedef struct { + bool as_geo; + const char* x_key; + const char* y_key; + RecordObject::field_t x_field; + RecordObject::field_t y_field; + } geo_data_t; + + typedef struct { + Subscriber::msgRef_t ref; + RecordObject* record; + int rows; + } batch_t; + typedef struct { char filename[FILE_NAME_MAX_LEN]; long size; @@ -115,34 +137,6 @@ class ParquetBuilder: public LuaObject private: - /*-------------------------------------------------------------------- - * Types - *--------------------------------------------------------------------*/ - - typedef List field_list_t; - typedef field_list_t::Iterator field_iterator_t; - - typedef struct { - bool as_geo; - const char* x_key; - const char* y_key; - RecordObject::field_t x_field; - RecordObject::field_t y_field; - } geo_data_t; - - typedef struct WKBPoint { - uint8_t byteOrder; - uint32_t wkbType; - double x; - double y; - } ALIGN_PACKED wkbpoint_t; - - typedef struct { - Subscriber::msgRef_t ref; - RecordObject* record; - int rows; - } batch_t; - /*-------------------------------------------------------------------- * Data *--------------------------------------------------------------------*/ @@ -152,10 +146,8 @@ class ParquetBuilder: public LuaObject bool active; Subscriber* inQ; const char* recType; - const char* batchRecType; Ordering recordBatch; field_list_t fieldList; - field_iterator_t* fieldIterator; Publisher* outQ; int rowSizeBytes; int batchRowSizeBytes; @@ -164,8 +156,7 @@ class ParquetBuilder: public LuaObject const char* outputPath; // final destination of the file geo_data_t geoData; - struct impl; // arrow implementation - impl* pimpl; // private arrow data + ArrowImpl* impl; // private arrow data /*-------------------------------------------------------------------- * Methods @@ -177,7 +168,7 @@ class ParquetBuilder: public LuaObject ~ParquetBuilder (void); static void* builderThread (void* parm); - void processRecordBatch (int num_rows); + void clearBatch (uint32_t trace_id); bool send2S3 (const char* s3dst); bool send2Client (void); }; From 44cefd1ddcdeac5d64fbcf4b9cf9c05a4f64d544 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Tue, 27 Feb 2024 12:49:26 +0000 Subject: [PATCH 22/43] more consistent use of asset_name when determining how to get arrow parms --- packages/arrow/ArrowParms.cpp | 52 +++++++++++++++---------------- packages/arrow/ParquetBuilder.cpp | 14 ++++----- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/packages/arrow/ArrowParms.cpp b/packages/arrow/ArrowParms.cpp index 410f95fb1..0544ba68f 100644 --- a/packages/arrow/ArrowParms.cpp +++ b/packages/arrow/ArrowParms.cpp @@ -138,40 +138,38 @@ ArrowParms::ArrowParms (lua_State* L, int index): lua_pop(L, 1); #ifdef __aws__ - /* Region */ - lua_getfield(L, index, REGION); - region = StringLib::duplicate(LuaObject::getLuaString(L, -1, true, NULL, &field_provided)); - if(region) - { - mlog(DEBUG, "Setting %s to %s", REGION, region); - } - else if(asset_name != NULL) + if(asset_name) { + /* Get Asset */ Asset* asset = dynamic_cast(LuaObject::getLuaObjectByName(asset_name, Asset::OBJECT_TYPE)); + + /* Region */ region = StringLib::duplicate(asset->getRegion()); - asset->releaseLuaObject(); - } - lua_pop(L, 1); + if(region) mlog(DEBUG, "Setting %s to %s from asset %s", REGION, region, asset_name); + else mlog(ERROR, "Failed to get region from asset %s", asset_name); - /* AWS Credentials */ - lua_getfield(L, index, CREDENTIALS); - credentials.fromLua(L, -1); - if(credentials.provided) - { - mlog(DEBUG, "Setting %s from user", CREDENTIALS); + /* Credentials */ + credentials = CredentialStore::get(asset->getIdentity()); + if(credentials.provided) mlog(DEBUG, "Setting %s from asset %s", CREDENTIALS, asset_name); + else mlog(ERROR, "Failed to get credentials from asset %s", asset_name); + + /* Release Asset */ + asset->releaseLuaObject(); } - else if(asset_name != NULL) + else { - Asset* asset = dynamic_cast(LuaObject::getLuaObjectByName(asset_name, Asset::OBJECT_TYPE)); - const char* identity = asset->getIdentity(); - credentials = CredentialStore::get(identity); - asset->releaseLuaObject(); - if(credentials.provided) - { - mlog(DEBUG, "Setting %s from asset %s", CREDENTIALS, asset_name); - } + /* Region */ + lua_getfield(L, index, REGION); + region = StringLib::duplicate(LuaObject::getLuaString(L, -1, true, NULL, &field_provided)); + if(region) mlog(DEBUG, "Setting %s to %s", REGION, region); + lua_pop(L, 1); + + /* AWS Credentials */ + lua_getfield(L, index, CREDENTIALS); + credentials.fromLua(L, -1); + if(credentials.provided) mlog(DEBUG, "Setting %s from user", CREDENTIALS); + lua_pop(L, 1); } - lua_pop(L, 1); #endif } } diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index e7315f64f..4fa717ab0 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -175,15 +175,9 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, assert(rec_type); assert(id); - /* Check Path */ - if((parms->path == NULL) || (parms->path[0] == '\0')) + /* Get Path */ + if(parms->asset_name) { - /* Check Asset Provided */ - if(!parms->asset_name) - { - throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to determine output path without asset"); - } - /* Check Private Cluster */ if(OsApi::getIsPublic()) { @@ -202,6 +196,10 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, outputPath = path_str.c_str(true); mlog(INFO, "Generating unique path: %s", outputPath); } + else if((parms->path == NULL) || (parms->path[0] == '\0')) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to determine output path"); + } else { outputPath = StringLib::duplicate(parms->path); From 48638419f17c7e3251e31db33f0b83f37e7032bc Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Wed, 28 Feb 2024 13:36:58 +0000 Subject: [PATCH 23/43] rework code to be better encapsulated in arrow implementation --- CMakeLists.txt | 5 - packages/arrow/ArrowImpl.cpp | 211 ++++++++++----------- packages/arrow/ArrowImpl.h | 37 ++-- packages/arrow/ArrowParms.cpp | 46 +++++ packages/arrow/ArrowParms.h | 3 + packages/arrow/ParquetBuilder.cpp | 61 +++++- packages/arrow/ParquetBuilder.h | 18 +- plugins/icesat2/plugin/AncillaryFields.cpp | 16 ++ 8 files changed, 253 insertions(+), 144 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f0c8c37c8..f14b514b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,11 +63,6 @@ if (${ENABLE_H5CORO_ATTRIBUTE_SUPPORT}) target_compile_definitions (slideruleLib PUBLIC H5CORO_ATTRIBUTE_SUPPORT) endif () -if (${ENABLE_APACHE_ARROW_10_COMPAT}) - message (STATUS "Enabling Apache Arrow 10 compatibility") - target_compile_definitions (slideruleLib PUBLIC APACHE_ARROW_10_COMPAT) -endif () - if (${ENABLE_BEST_EFFORT_CONDA_ENV}) message (STATUS "Attempting best effort at running in a mixed system and conda environment") target_compile_definitions (slideruleLib PUBLIC BEST_EFFORT_CONDA_ENV) diff --git a/packages/arrow/ArrowImpl.cpp b/packages/arrow/ArrowImpl.cpp index 0b100ea4f..5577e5798 100644 --- a/packages/arrow/ArrowImpl.cpp +++ b/packages/arrow/ArrowImpl.cpp @@ -49,58 +49,18 @@ /*---------------------------------------------------------------------------- * Constructor *----------------------------------------------------------------------------*/ -ArrowImpl::ArrowImpl (ParquetBuilder::field_list_t& field_list, - const ParquetBuilder::geo_data_t& geo_data, - const char* rec_type, - const char* index_key, - const char* file_name) +ArrowImpl::ArrowImpl (ParquetBuilder* _builder): + parquetBuilder(_builder), + schema(NULL), + fieldList(LIST_BLOCK_SIZE), + fieldIterator(NULL), + batchRecType(NULL), + firstTime(true) { - /* Initialize Data */ - batchRecType = NULL; - - /* Define Table Schema */ - vector> schema_vector; - addFieldsToSchema(schema_vector, field_list, &batchRecType, geo_data, rec_type, 0, 0); - if(geo_data.as_geo) schema_vector.push_back(arrow::field("geometry", arrow::binary())); - schema = make_shared(schema_vector); - fieldIterator = new field_iterator_t(field_list); - - /* Create Arrow Output Stream */ - shared_ptr file_output_stream; - PARQUET_ASSIGN_OR_THROW(file_output_stream, arrow::io::FileOutputStream::Open(file_name)); - - /* Create Writer Properties */ - parquet::WriterProperties::Builder writer_props_builder; - writer_props_builder.compression(parquet::Compression::SNAPPY); - writer_props_builder.version(parquet::ParquetVersion::PARQUET_2_6); - shared_ptr writer_props = writer_props_builder.build(); - - /* Create Arrow Writer Properties */ - auto arrow_writer_props = parquet::ArrowWriterProperties::Builder().store_schema()->build(); - - /* Build GeoParquet MetaData */ - auto metadata = schema->metadata() ? schema->metadata()->Copy() : std::make_shared(); - if(geo_data.as_geo) appendGeoMetaData(metadata); - appendServerMetaData(metadata); - appendPandasMetaData(metadata, schema, fieldIterator, index_key, geo_data.as_geo); - schema = schema->WithMetadata(metadata); - - /* Create Parquet Writer */ - #ifdef APACHE_ARROW_10_COMPAT - (void)parquet::arrow::FileWriter::Open(*schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props, &parquetWriter); - #elif 0 // alternative method of creating file writer - std::shared_ptr parquet_schema; - (void)parquet::arrow::ToParquetSchema(schema.get(), *writer_props, *arrow_writer_props, &parquet_schema); - auto schema_node = std::static_pointer_cast(parquet_schema->schema_root()); - std::unique_ptr base_writer; - base_writer = parquet::ParquetFileWriter::Open(std::move(file_output_stream), schema_node, std::move(writer_props), metadata); - auto schema_ptr = std::make_shared<::arrow::Schema>(*schema); - (void)parquet::arrow::FileWriter::Make(::arrow::default_memory_pool(), std::move(base_writer), std::move(schema_ptr), std::move(arrow_writer_props), &parquetWriter); - #else - arrow::Result> result = parquet::arrow::FileWriter::Open(*schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props); - if(result.ok()) parquetWriter = std::move(result).ValueOrDie(); - else mlog(CRITICAL, "Failed to open parquet writer: %s", result.status().ToString().c_str()); - #endif + /* Build Field List and Iterator */ + buildFieldList(parquetBuilder->getRecType(), 0, 0); + if(parquetBuilder->getAsGeo()) fieldVector.push_back(arrow::field("geometry", arrow::binary())); + fieldIterator = new field_iterator_t(fieldList); } /*---------------------------------------------------------------------------- @@ -166,21 +126,28 @@ bool ArrowImpl::processRecordBatch (Ordering& record_ba stop_trace(INFO, geo_trace_id); } - /* Build and Write Table */ - uint32_t write_trace_id = start_trace(INFO, trace_id, "write_table", "%s", "{}"); + /* Create Parquet Writer (on first time) */ + if(firstTime) + { + createSchema(); + firstTime = false; + } + if(parquetWriter) { + /* Build and Write Table */ + uint32_t write_trace_id = start_trace(INFO, trace_id, "write_table", "%s", "{}"); shared_ptr table = arrow::Table::Make(schema, columns); arrow::Status s = parquetWriter->WriteTable(*table, num_rows); if(s.ok()) status = true; else mlog(CRITICAL, "Failed to write parquet table: %s", s.CodeAsString().c_str()); - } - stop_trace(INFO, write_trace_id); + stop_trace(INFO, write_trace_id); - /* Close Parquet Writer */ - if(file_finished) - { - (void)parquetWriter->Close(); + /* Close Parquet Writer */ + if(file_finished) + { + (void)parquetWriter->Close(); + } } /* Stop Trace */ @@ -195,15 +162,46 @@ bool ArrowImpl::processRecordBatch (Ordering& record_ba ******************************************************************************/ /*---------------------------------------------------------------------------- -* addFieldsToSchema +* createSchema +*----------------------------------------------------------------------------*/ +bool ArrowImpl::createSchema (void) +{ + /* Set Arrow Output Stream */ + shared_ptr file_output_stream; + PARQUET_ASSIGN_OR_THROW(file_output_stream, arrow::io::FileOutputStream::Open(parquetBuilder->getFileName())); + + /* Set Writer Properties */ + parquet::WriterProperties::Builder writer_props_builder; + writer_props_builder.compression(parquet::Compression::SNAPPY); + writer_props_builder.version(parquet::ParquetVersion::PARQUET_2_6); + shared_ptr writer_props = writer_props_builder.build(); + + /* Set Arrow Writer Properties */ + auto arrow_writer_props = parquet::ArrowWriterProperties::Builder().store_schema()->build(); + + /* Create Schema */ + schema = make_shared(fieldVector); + + /* Set MetaData */ + auto metadata = schema->metadata() ? schema->metadata()->Copy() : std::make_shared(); + if(parquetBuilder->getAsGeo()) appendGeoMetaData(metadata); + appendServerMetaData(metadata); + appendPandasMetaData(metadata); + schema = schema->WithMetadata(metadata); + + /* Create Parquet Writer */ + arrow::Result> result = parquet::arrow::FileWriter::Open(*schema, ::arrow::default_memory_pool(), file_output_stream, writer_props, arrow_writer_props); + if(result.ok()) parquetWriter = std::move(result).ValueOrDie(); + else mlog(CRITICAL, "Failed to open parquet writer: %s", result.status().ToString().c_str()); + + /* Return Status */ + return parquetWriter != NULL; +} + +/*---------------------------------------------------------------------------- +* buildFieldList *----------------------------------------------------------------------------*/ -bool ArrowImpl::addFieldsToSchema ( vector>& schema_vector, - ParquetBuilder::field_list_t& field_list, - const char** batch_rec_type, - const ParquetBuilder::geo_data_t& geo_data, - const char* rec_type, - int offset, - int flags ) +bool ArrowImpl::buildFieldList (const char* rec_type, int offset, int flags) { /* Loop Through Fields in Record */ Dictionary* fields = RecordObject::getRecordFields(rec_type); @@ -216,9 +214,9 @@ bool ArrowImpl::addFieldsToSchema ( vector>& schema_vec bool add_field_to_list = true; /* Check for Geometry Columns */ - if(geo_data.as_geo) + if(parquetBuilder->getAsGeo()) { - if(field.offset == geo_data.x_field.offset || field.offset == geo_data.y_field.offset) + if(field.offset == parquetBuilder->getXField().offset || field.offset == parquetBuilder->getYField().offset) { /* skip over source columns for geometry as they will be added * separately as a part of the dedicated geometry column */ @@ -227,9 +225,9 @@ bool ArrowImpl::addFieldsToSchema ( vector>& schema_vec } /* Check for Batch Record Type */ - if((*batch_rec_type == NULL) && (field.flags & RecordObject::BATCH)) + if((batchRecType == NULL) && (field.flags & RecordObject::BATCH)) { - *batch_rec_type = StringLib::duplicate(field.exttype); + batchRecType = StringLib::duplicate(field.exttype); } /* Add to Schema */ @@ -237,20 +235,20 @@ bool ArrowImpl::addFieldsToSchema ( vector>& schema_vec { switch(field.type) { - case RecordObject::INT8: schema_vector.push_back(arrow::field(field_name, arrow::int8())); break; - case RecordObject::INT16: schema_vector.push_back(arrow::field(field_name, arrow::int16())); break; - case RecordObject::INT32: schema_vector.push_back(arrow::field(field_name, arrow::int32())); break; - case RecordObject::INT64: schema_vector.push_back(arrow::field(field_name, arrow::int64())); break; - case RecordObject::UINT8: schema_vector.push_back(arrow::field(field_name, arrow::uint8())); break; - case RecordObject::UINT16: schema_vector.push_back(arrow::field(field_name, arrow::uint16())); break; - case RecordObject::UINT32: schema_vector.push_back(arrow::field(field_name, arrow::uint32())); break; - case RecordObject::UINT64: schema_vector.push_back(arrow::field(field_name, arrow::uint64())); break; - case RecordObject::FLOAT: schema_vector.push_back(arrow::field(field_name, arrow::float32())); break; - case RecordObject::DOUBLE: schema_vector.push_back(arrow::field(field_name, arrow::float64())); break; - case RecordObject::TIME8: schema_vector.push_back(arrow::field(field_name, arrow::timestamp(arrow::TimeUnit::NANO))); break; - case RecordObject::STRING: schema_vector.push_back(arrow::field(field_name, arrow::utf8())); break; - - case RecordObject::USER: addFieldsToSchema(schema_vector, field_list, batch_rec_type, geo_data, field.exttype, field.offset, field.flags); + case RecordObject::INT8: fieldVector.push_back(arrow::field(field_name, arrow::int8())); break; + case RecordObject::INT16: fieldVector.push_back(arrow::field(field_name, arrow::int16())); break; + case RecordObject::INT32: fieldVector.push_back(arrow::field(field_name, arrow::int32())); break; + case RecordObject::INT64: fieldVector.push_back(arrow::field(field_name, arrow::int64())); break; + case RecordObject::UINT8: fieldVector.push_back(arrow::field(field_name, arrow::uint8())); break; + case RecordObject::UINT16: fieldVector.push_back(arrow::field(field_name, arrow::uint16())); break; + case RecordObject::UINT32: fieldVector.push_back(arrow::field(field_name, arrow::uint32())); break; + case RecordObject::UINT64: fieldVector.push_back(arrow::field(field_name, arrow::uint64())); break; + case RecordObject::FLOAT: fieldVector.push_back(arrow::field(field_name, arrow::float32())); break; + case RecordObject::DOUBLE: fieldVector.push_back(arrow::field(field_name, arrow::float64())); break; + case RecordObject::TIME8: fieldVector.push_back(arrow::field(field_name, arrow::timestamp(arrow::TimeUnit::NANO))); break; + case RecordObject::STRING: fieldVector.push_back(arrow::field(field_name, arrow::utf8())); break; + + case RecordObject::USER: buildFieldList(field.exttype, field.offset, field.flags); add_field_to_list = false; break; @@ -262,18 +260,18 @@ bool ArrowImpl::addFieldsToSchema ( vector>& schema_vec { switch(field.type) { - case RecordObject::INT8: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int8()))); break; - case RecordObject::INT16: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int16()))); break; - case RecordObject::INT32: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int32()))); break; - case RecordObject::INT64: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::int64()))); break; - case RecordObject::UINT8: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint8()))); break; - case RecordObject::UINT16: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint16()))); break; - case RecordObject::UINT32: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint32()))); break; - case RecordObject::UINT64: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::uint64()))); break; - case RecordObject::FLOAT: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::float32()))); break; - case RecordObject::DOUBLE: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::float64()))); break; - case RecordObject::TIME8: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::timestamp(arrow::TimeUnit::NANO)))); break; - case RecordObject::STRING: schema_vector.push_back(arrow::field(field_name, arrow::list(arrow::utf8()))); break; + case RecordObject::INT8: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::int8()))); break; + case RecordObject::INT16: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::int16()))); break; + case RecordObject::INT32: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::int32()))); break; + case RecordObject::INT64: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::int64()))); break; + case RecordObject::UINT8: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::uint8()))); break; + case RecordObject::UINT16: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::uint16()))); break; + case RecordObject::UINT32: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::uint32()))); break; + case RecordObject::UINT64: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::uint64()))); break; + case RecordObject::FLOAT: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::float32()))); break; + case RecordObject::DOUBLE: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::float64()))); break; + case RecordObject::TIME8: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::timestamp(arrow::TimeUnit::NANO)))); break; + case RecordObject::STRING: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::utf8()))); break; case RecordObject::USER: // arrays of user data types (i.e. nested structures) are not supported add_field_to_list = false; @@ -290,7 +288,7 @@ bool ArrowImpl::addFieldsToSchema ( vector>& schema_vec RecordObject::field_t column_field = field; column_field.offset += offset; column_field.flags |= flags; - field_list.add(column_field); + fieldList.add(column_field); } } @@ -423,11 +421,7 @@ void ArrowImpl::appendServerMetaData (const std::shared_ptr& metadata, - const shared_ptr& _schema, - const field_iterator_t* field_iterator, - const char* index_key, - bool as_geo ) +void ArrowImpl::appendPandasMetaData (const std::shared_ptr& metadata) { /* Initialize Pandas Meta Data String */ string pandasstr(R"json({ @@ -449,7 +443,7 @@ void ArrowImpl::appendPandasMetaData ( const std::shared_ptrfield_names()) + for(const std::string& field_name: schema->field_names()) { /* Initialize Column String */ string columnstr(R"json({"name": "_NAME_", "field_name": "_NAME_", "pandas_type": "_PTYPE_", "numpy_type": "_NTYPE_", "metadata": null})json"); @@ -457,10 +451,10 @@ void ArrowImpl::appendPandasMetaData ( const std::shared_ptrlength) + if(index < fieldIterator->length) { /* Add Column from Field List */ - RecordObject::field_t field = (*field_iterator)[index++]; + RecordObject::field_t field = (*fieldIterator)[index++]; switch(field.type) { case RecordObject::DOUBLE: pandas_type = "float64"; numpy_type = "float64"; break; @@ -479,12 +473,12 @@ void ArrowImpl::appendPandasMetaData ( const std::shared_ptrlength)) + if(!parquetBuilder->getAsGeo() && (index == fieldIterator->length)) { is_last_entry = true; } } - else if(as_geo && StringLib::match(field_name.c_str(), "geometry")) + else if(parquetBuilder->getAsGeo() && StringLib::match(field_name.c_str(), "geometry")) { /* Add Column for Geometry */ pandas_type = "bytes"; @@ -508,6 +502,7 @@ void ArrowImpl::appendPandasMetaData ( const std::shared_ptrgetIndexKey(); FString indexstr("\"%s\"", index_key ? index_key : ""); /* Fill In Pandas Meta Data String */ diff --git a/packages/arrow/ArrowImpl.h b/packages/arrow/ArrowImpl.h index 72fe46078..f0e51c872 100644 --- a/packages/arrow/ArrowImpl.h +++ b/packages/arrow/ArrowImpl.h @@ -68,11 +68,7 @@ class ArrowImpl * Methods *--------------------------------------------------------------------*/ - ArrowImpl (ParquetBuilder::field_list_t& field_list, - const ParquetBuilder::geo_data_t& geo_data, - const char* rec_type, - const char* index_key, - const char* file_name); + explicit ArrowImpl (ParquetBuilder* _builder); ~ArrowImpl (void); bool isValid (void); @@ -85,11 +81,18 @@ class ArrowImpl private: + /*-------------------------------------------------------------------- + * Constants + *--------------------------------------------------------------------*/ + + static const int LIST_BLOCK_SIZE = 32; + /*-------------------------------------------------------------------- * Types *--------------------------------------------------------------------*/ - typedef ParquetBuilder::field_list_t::Iterator field_iterator_t; + typedef List field_list_t; + typedef field_list_t::Iterator field_iterator_t; typedef struct WKBPoint { uint8_t byteOrder; @@ -102,29 +105,24 @@ class ArrowImpl * Data *--------------------------------------------------------------------*/ + ParquetBuilder* parquetBuilder; shared_ptr schema; unique_ptr parquetWriter; + vector> fieldVector; + field_list_t fieldList; field_iterator_t* fieldIterator; const char* batchRecType; + bool firstTime; /*-------------------------------------------------------------------- * Methods *--------------------------------------------------------------------*/ - bool addFieldsToSchema (vector>& schema_vector, - ParquetBuilder::field_list_t& field_list, - const char** batch_rec_type, - const ParquetBuilder::geo_data_t& geo, - const char* rec_type, - int offset, - int flags); + bool createSchema (void); + bool buildFieldList (const char* rec_type, int offset, int flags); void appendGeoMetaData (const std::shared_ptr& metadata); void appendServerMetaData (const std::shared_ptr& metadata); - void appendPandasMetaData (const std::shared_ptr& metadata, - const shared_ptr& _schema, - const field_iterator_t* field_iterator, - const char* index_key, - bool as_geo); + void appendPandasMetaData (const std::shared_ptr& metadata); void processField (RecordObject::field_t& field, shared_ptr* column, Ordering& record_batch, @@ -140,9 +138,6 @@ class ArrowImpl Ordering& record_batch, int num_rows, int batch_row_size_bits); - - - }; #endif /* __arrow_impl__ */ diff --git a/packages/arrow/ArrowParms.cpp b/packages/arrow/ArrowParms.cpp index 0544ba68f..870c13164 100644 --- a/packages/arrow/ArrowParms.cpp +++ b/packages/arrow/ArrowParms.cpp @@ -45,6 +45,7 @@ const char* ArrowParms::PATH = "path"; const char* ArrowParms::FORMAT = "format"; const char* ArrowParms::OPEN_ON_COMPLETE = "open_on_complete"; const char* ArrowParms::AS_GEO = "as_geo"; +const char* ArrowParms::ANCILLARY = "ancillary"; const char* ArrowParms::ASSET = "asset"; const char* ArrowParms::REGION = "region"; const char* ArrowParms::CREDENTIALS = "credentials"; @@ -131,6 +132,12 @@ ArrowParms::ArrowParms (lua_State* L, int index): if(field_provided) mlog(DEBUG, "Setting %s to %d", AS_GEO, (int)as_geo); lua_pop(L, 1); + /* Ancillary */ + lua_getfield(L, index, ANCILLARY); + luaGetAncillary(L, -1, &field_provided); + if(field_provided) mlog(DEBUG, "Setting %s to user provided list", ANCILLARY); + lua_pop(L, 1); + /* Asset */ lua_getfield(L, index, ASSET); asset_name = StringLib::duplicate(LuaObject::getLuaString(L, -1, true, NULL, &field_provided)); @@ -306,3 +313,42 @@ int ArrowParms::luaPath (lua_State* L) return luaL_error(L, "method invoked from invalid object: %s", __FUNCTION__); } } + +/*---------------------------------------------------------------------------- + * luaGetAncillary + *----------------------------------------------------------------------------*/ +void ArrowParms::luaGetAncillary (lua_State* L, int index, bool* provided) +{ + /* Reset Provided */ + if(provided) *provided = false; + + /* Must be table of strings */ + if(lua_istable(L, index)) + { + /* Get number of fields in table */ + int num_fields = lua_rawlen(L, index); + if(num_fields > 0 && provided) *provided = true; + + /* Iterate through each field in table */ + for(int i = 0; i < num_fields; i++) + { + /* Get field */ + lua_rawgeti(L, index, i+1); + + /* Set field */ + if(lua_isstring(L, -1)) + { + const char* field_str = LuaObject::getLuaString(L, -1); + string field_name(field_str); + ancillary_fields.push_back(field_name); + } + + /* Clean up stack */ + lua_pop(L, 1); + } + } + else if(!lua_isnil(L, index)) + { + mlog(ERROR, "ancillary fields must be provided as a table of strings"); + } +} diff --git a/packages/arrow/ArrowParms.h b/packages/arrow/ArrowParms.h index e7be8f56d..aa10f7aa3 100644 --- a/packages/arrow/ArrowParms.h +++ b/packages/arrow/ArrowParms.h @@ -80,6 +80,7 @@ class ArrowParms: public LuaObject static const char* FORMAT; static const char* OPEN_ON_COMPLETE; static const char* AS_GEO; + static const char* ANCILLARY; static const char* ASSET; static const char* REGION; static const char* CREDENTIALS; @@ -98,6 +99,7 @@ class ArrowParms: public LuaObject bool as_geo; // whether to create a standard geo-based formatted file const char* asset_name; const char* region; + vector ancillary_fields; #ifdef __aws__ CredentialStore::Credential credentials; @@ -124,6 +126,7 @@ class ArrowParms: public LuaObject static int luaIsParquet (lua_State* L); static int luaIsCSV (lua_State* L); static int luaPath (lua_State* L); + void luaGetAncillary (lua_State* L, int index, bool* provided); }; #endif /* __arrow_parms__ */ diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index 4fa717ab0..c0ba118e7 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -153,6 +153,54 @@ void ParquetBuilder::deinit (void) { } +/*---------------------------------------------------------------------------- + * getFileName + *----------------------------------------------------------------------------*/ +const char* ParquetBuilder::getFileName (void) +{ + return fileName; +} + +/*---------------------------------------------------------------------------- + * getRecType + *----------------------------------------------------------------------------*/ +const char* ParquetBuilder::getRecType (void) +{ + return recType; +} + +/*---------------------------------------------------------------------------- + * getIndexKey + *----------------------------------------------------------------------------*/ +const char* ParquetBuilder::getIndexKey (void) +{ + return indexKey; +} +/*---------------------------------------------------------------------------- + + * getAsGeo + *----------------------------------------------------------------------------*/ +bool ParquetBuilder::getAsGeo (void) +{ + return geoData.as_geo; +} + +/*---------------------------------------------------------------------------- + * getXField + *----------------------------------------------------------------------------*/ +RecordObject::field_t& ParquetBuilder::getXField (void) +{ + return geoData.x_field; +} + +/*---------------------------------------------------------------------------- + * getYField + *----------------------------------------------------------------------------*/ +RecordObject::field_t& ParquetBuilder::getYField (void) +{ + return geoData.y_field; +} + /****************************************************************************** * PRIVATE METHODS *******************************************************************************/ @@ -166,7 +214,6 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, LuaObject(L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), parms(_parms), recType(StringLib::duplicate(rec_type)), - fieldList(LIST_BLOCK_SIZE), geoData(geo) { assert(_parms); @@ -209,8 +256,11 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, FString tmp_file("%s%s.parquet", TMP_FILE_PREFIX, id); fileName = tmp_file.c_str(true); + /* Save Index Key */ + indexKey = StringLib::duplicate(index_key); + /* Allocate Implementation */ - impl = new ArrowImpl(fieldList, geoData, recType, index_key, fileName); + impl = new ArrowImpl(this); /* Row Based Parameters */ batchRowSizeBytes = RecordObject::getRecordDataSize(impl->getBatchRecType()); @@ -246,6 +296,7 @@ ParquetBuilder::~ParquetBuilder(void) delete [] fileName; delete [] outputPath; delete [] recType; + delete [] indexKey; delete outQ; delete inQ; delete impl; @@ -315,7 +366,11 @@ void* ParquetBuilder::builderThread(void* parm) if(row_cnt >= builder->maxRowsInGroup) { bool status = builder->impl->processRecordBatch(builder->recordBatch, row_cnt, builder->batchRowSizeBytes * 8, builder->geoData); - if(!status) alert(RTE_ERROR, INFO, builder->outQ, NULL, "Failed to process record batch for %s", builder->outputPath); + if(!status) + { + alert(RTE_ERROR, INFO, builder->outQ, NULL, "Failed to process record batch for %s", builder->outputPath); + builder->active = false; // breaks out of loop + } builder->clearBatch(trace_id); row_cnt = 0; } diff --git a/packages/arrow/ParquetBuilder.h b/packages/arrow/ParquetBuilder.h index 4ccbea029..17c3cf5b1 100644 --- a/packages/arrow/ParquetBuilder.h +++ b/packages/arrow/ParquetBuilder.h @@ -70,7 +70,6 @@ class ParquetBuilder: public LuaObject * Constants *--------------------------------------------------------------------*/ - static const int LIST_BLOCK_SIZE = 32; static const int FILE_NAME_MAX_LEN = 128; static const int URL_MAX_LEN = 512; static const int FILE_BUFFER_RSPS_SIZE = 0x2000000; // 32MB @@ -96,8 +95,6 @@ class ParquetBuilder: public LuaObject * Types *--------------------------------------------------------------------*/ - typedef List field_list_t; - typedef struct { bool as_geo; const char* x_key; @@ -131,9 +128,16 @@ class ParquetBuilder: public LuaObject * Methods *--------------------------------------------------------------------*/ - static int luaCreate (lua_State* L); - static void init (void); - static void deinit (void); + static int luaCreate (lua_State* L); + static void init (void); + static void deinit (void); + + const char* getFileName (void); + const char* getRecType (void); + const char* getIndexKey (void); + bool getAsGeo (void); + RecordObject::field_t& getXField (void); + RecordObject::field_t& getYField (void); private: @@ -146,8 +150,8 @@ class ParquetBuilder: public LuaObject bool active; Subscriber* inQ; const char* recType; + const char* indexKey; Ordering recordBatch; - field_list_t fieldList; Publisher* outQ; int rowSizeBytes; int batchRowSizeBytes; diff --git a/plugins/icesat2/plugin/AncillaryFields.cpp b/plugins/icesat2/plugin/AncillaryFields.cpp index 1b4cf2a84..629e5b814 100644 --- a/plugins/icesat2/plugin/AncillaryFields.cpp +++ b/plugins/icesat2/plugin/AncillaryFields.cpp @@ -44,6 +44,14 @@ * STATIC DATA ******************************************************************************/ +/* + * Ancillary Field Records + * + * This record is used to capture a set of different fields in the source granule, + * all associated with a single extent id. For example, if there was an ancillary + * field request for fields X, Y, and Z, then this record would hold the values + * for X, Y, and Z all in a single record and associate it with the extent. + */ const char* AncillaryFields::ancFieldRecType = "ancfrec.field"; const RecordObject::fieldDef_t AncillaryFields::ancFieldRecDef[] = { {"anc_type", RecordObject::UINT8, offsetof(field_t, anc_type), 1, NULL, NATIVE_FLAGS}, @@ -59,6 +67,14 @@ const RecordObject::fieldDef_t AncillaryFields::ancFieldArrayRecDef[] = { {"fields", RecordObject::USER, offsetof(field_array_t, fields), 0, ancFieldRecType, NATIVE_FLAGS | RecordObject::BATCH} }; +/* + * Ancillary Element Records + * + * This record is used to capture an array of field values all associated with a single field. + * It is primarily used for the ATL03 photon data and things like that where there is a variable + * number of values associated with a given field for a given extent. So wherease the Ancillary + * Field Record is multiple fields each with one value; this is multiple values for just one field. + */ const char* AncillaryFields::ancElementRecType = "ancerec"; const RecordObject::fieldDef_t AncillaryFields::ancElementRecDef[] = { {"extent_id", RecordObject::UINT64, offsetof(element_array_t, extent_id), 1, NULL, NATIVE_FLAGS}, From 00ecdb34d3cefca4ca35b232c149e32abac05567 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Wed, 28 Feb 2024 15:29:50 +0000 Subject: [PATCH 24/43] fixed parquet builder rework; more efficient wait for iam role credentials in server script --- packages/arrow/ArrowImpl.cpp | 13 +++---------- packages/arrow/ArrowImpl.h | 1 - packages/arrow/ParquetBuilder.cpp | 12 ++---------- plugins/swot/endpoints/swotl2p.lua | 2 +- scripts/apps/server.lua | 12 +++++++++++- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/packages/arrow/ArrowImpl.cpp b/packages/arrow/ArrowImpl.cpp index 5577e5798..5ed7de6fb 100644 --- a/packages/arrow/ArrowImpl.cpp +++ b/packages/arrow/ArrowImpl.cpp @@ -72,14 +72,6 @@ ArrowImpl::~ArrowImpl (void) delete [] batchRecType; } -/*---------------------------------------------------------------------------- - * isValid - *----------------------------------------------------------------------------*/ -bool ArrowImpl::isValid (void) -{ - return parquetWriter != NULL; -} - /*---------------------------------------------------------------------------- * getBatchRecType *----------------------------------------------------------------------------*/ @@ -231,7 +223,7 @@ bool ArrowImpl::buildFieldList (const char* rec_type, int offset, int flags) } /* Add to Schema */ - if(field.elements == 1 || field.type == RecordObject::USER) + if(field.elements == 1) { switch(field.type) { @@ -273,7 +265,8 @@ bool ArrowImpl::buildFieldList (const char* rec_type, int offset, int flags) case RecordObject::TIME8: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::timestamp(arrow::TimeUnit::NANO)))); break; case RecordObject::STRING: fieldVector.push_back(arrow::field(field_name, arrow::list(arrow::utf8()))); break; - case RecordObject::USER: // arrays of user data types (i.e. nested structures) are not supported + case RecordObject::USER: if(field.flags & RecordObject::BATCH) buildFieldList(field.exttype, field.offset, field.flags); + else mlog(CRITICAL, "User fields that are arrays must be identified as batches: %s", field.exttype); add_field_to_list = false; break; diff --git a/packages/arrow/ArrowImpl.h b/packages/arrow/ArrowImpl.h index f0e51c872..b5c51aa12 100644 --- a/packages/arrow/ArrowImpl.h +++ b/packages/arrow/ArrowImpl.h @@ -71,7 +71,6 @@ class ArrowImpl explicit ArrowImpl (ParquetBuilder* _builder); ~ArrowImpl (void); - bool isValid (void); const char* getBatchRecType (void); bool processRecordBatch (Ordering& record_batch, int num_rows, diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index c0ba118e7..a4f6d44fb 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -273,16 +273,8 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, inQ = new Subscriber(inq_name, MsgQ::SUBSCRIBER_OF_CONFIDENCE, qdepth); /* Start Builder Thread */ - if(impl->isValid()) - { - active = true; - builderPid = new Thread(builderThread, this); - } - else - { - active = false; - builderPid = NULL; - } + active = true; + builderPid = new Thread(builderThread, this); } /*---------------------------------------------------------------------------- diff --git a/plugins/swot/endpoints/swotl2p.lua b/plugins/swot/endpoints/swotl2p.lua index f75d4b056..e58aa265f 100644 --- a/plugins/swot/endpoints/swotl2p.lua +++ b/plugins/swot/endpoints/swotl2p.lua @@ -9,4 +9,4 @@ local rqst = json.decode(arg[1]) local resources = rqst["resources"] local parms = rqst["parms"] -proxy.proxy(resources, parms, "swotl2", "swotl2var", nil, nil, nil) +proxy.proxy(resources, parms, "swotl2", "swotl2var", nil, nil) diff --git a/scripts/apps/server.lua b/scripts/apps/server.lua index 83665bf5f..1dba95e03 100644 --- a/scripts/apps/server.lua +++ b/scripts/apps/server.lua @@ -69,7 +69,17 @@ local assets = asset.loaddir(asset_directory) -- Run IAM Role Authentication Script (identity="iam-role") -- local role_auth_script = core.script("iam_role_auth"):name("RoleAuthScript") -sys.wait(5) -- best effort delay to give time for iam role to be established +local iam_role_max_wait = 10 +while not aws.csget("iam-role") do + iam_role_max_wait = iam_role_max_wait - 1 + if iam_role_max_wait == 0 then + print("Failed to establish IAM role credentials at startup") + break + else + print("Waiting to establish IAM role...") + sys.wait(1) + end +end -- Run Earth Data Authentication Scripts -- if authenticate_to_nsidc then From bd6a828c98935af48828be84b3bb38612a1a2d1d Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Wed, 28 Feb 2024 21:46:28 +0000 Subject: [PATCH 25/43] added metadata to record definitions to support parquet and raster sampling --- packages/arrow/ArrowImpl.cpp | 2 + packages/arrow/ParquetBuilder.cpp | 23 +++-- packages/core/ContainerRecord.cpp | 2 +- packages/core/RecordObject.cpp | 114 +++++++++++++++++++++-- packages/core/RecordObject.h | 37 ++++++-- plugins/gedi/plugin/Gedi01bReader.cpp | 8 +- plugins/gedi/plugin/Gedi02aReader.cpp | 8 +- plugins/gedi/plugin/Gedi04aReader.cpp | 8 +- plugins/icesat2/plugin/Atl03Reader.cpp | 8 +- plugins/icesat2/plugin/Atl06Dispatch.cpp | 8 +- plugins/icesat2/plugin/Atl06Reader.cpp | 8 +- plugins/icesat2/plugin/Atl08Dispatch.cpp | 8 +- plugins/swot/plugin/SwotL2Reader.cpp | 4 +- 13 files changed, 183 insertions(+), 55 deletions(-) diff --git a/packages/arrow/ArrowImpl.cpp b/packages/arrow/ArrowImpl.cpp index 5ed7de6fb..93b6e1fde 100644 --- a/packages/arrow/ArrowImpl.cpp +++ b/packages/arrow/ArrowImpl.cpp @@ -208,6 +208,8 @@ bool ArrowImpl::buildFieldList (const char* rec_type, int offset, int flags) /* Check for Geometry Columns */ if(parquetBuilder->getAsGeo()) { + +// TODO: this could just check for the x and y flags if(field.offset == parquetBuilder->getXField().offset || field.offset == parquetBuilder->getYField().offset) { /* skip over source columns for geometry as they will be added diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index a4f6d44fb..ac1a744f4 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -252,6 +252,19 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, outputPath = StringLib::duplicate(parms->path); } + /* Get Row Size */ + const char* batch_rec_field_name = RecordObject::getRecordBatchField(recType); + RecordObject::field_t batch_rec_field = RecordObject::getDefinedField(recType, batch_rec_field_name); + if(batch_rec_field.type == RecordObject::INVALID_FIELD) batchRowSizeBytes = 0; + else batchRowSizeBytes = RecordObject::getRecordDataSize(batch_rec_field.exttype); + rowSizeBytes = RecordObject::getRecordDataSize(recType) + batchRowSizeBytes; + maxRowsInGroup = ROW_GROUP_SIZE / rowSizeBytes; + + /* Initialize Queues */ + int qdepth = maxRowsInGroup * QUEUE_BUFFER_FACTOR; + outQ = new Publisher(outq_name, Publisher::defaultFree, qdepth); + inQ = new Subscriber(inq_name, MsgQ::SUBSCRIBER_OF_CONFIDENCE, qdepth); + /* Create Unique Temporary Filename */ FString tmp_file("%s%s.parquet", TMP_FILE_PREFIX, id); fileName = tmp_file.c_str(true); @@ -262,16 +275,6 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, /* Allocate Implementation */ impl = new ArrowImpl(this); - /* Row Based Parameters */ - batchRowSizeBytes = RecordObject::getRecordDataSize(impl->getBatchRecType()); - rowSizeBytes = RecordObject::getRecordDataSize(recType) + batchRowSizeBytes; - maxRowsInGroup = ROW_GROUP_SIZE / rowSizeBytes; - - /* Initialize Queues */ - int qdepth = maxRowsInGroup * QUEUE_BUFFER_FACTOR; - outQ = new Publisher(outq_name, Publisher::defaultFree, qdepth); - inQ = new Subscriber(inq_name, MsgQ::SUBSCRIBER_OF_CONFIDENCE, qdepth); - /* Start Builder Thread */ active = true; builderPid = new Thread(builderThread, this); diff --git a/packages/core/ContainerRecord.cpp b/packages/core/ContainerRecord.cpp index 89ae69ff3..1563568ce 100644 --- a/packages/core/ContainerRecord.cpp +++ b/packages/core/ContainerRecord.cpp @@ -60,8 +60,8 @@ RecordObject::fieldDef_t ContainerRecord::recDef[] = *----------------------------------------------------------------------------*/ void ContainerRecord::init (void) { - RECDEF(recType, recDef, sizeof(rec_t), NULL); RECDEF(entryRecType, entryRecDef, sizeof(entry_t), NULL); + RECDEF(recType, recDef, sizeof(rec_t), NULL); } /*---------------------------------------------------------------------------- diff --git a/packages/core/RecordObject.cpp b/packages/core/RecordObject.cpp index 66dda5342..ffc6df5f9 100644 --- a/packages/core/RecordObject.cpp +++ b/packages/core/RecordObject.cpp @@ -986,7 +986,10 @@ RecordObject::valType_t RecordObject::getValueType(const field_t& f) RecordObject::recordDefErr_t RecordObject::defineRecord(const char* rec_type, const char* id_field, int data_size, const fieldDef_t* fields, int num_fields, int max_fields) { assert(rec_type); - return addDefinition(NULL, rec_type, id_field, data_size, fields, num_fields, max_fields); + definition_t* rec_def = NULL; + recordDefErr_t status = addDefinition(&rec_def, rec_type, id_field, data_size, fields, num_fields, max_fields); + if(status == SUCCESS_DEF) scanDefinition(rec_def, "", rec_type); + return status; } /*---------------------------------------------------------------------------- @@ -1040,6 +1043,56 @@ const char* RecordObject::getRecordIdField(const char* rec_type) return def->id_field; } +/*---------------------------------------------------------------------------- + * getRecordIndexField + *----------------------------------------------------------------------------*/ +const char* RecordObject::getRecordIndexField (const char* rec_type) +{ + definition_t* def = getDefinition(rec_type); + if(def == NULL) return NULL; + return def->index_field; +} + +/*---------------------------------------------------------------------------- + * getRecordTimeField + *----------------------------------------------------------------------------*/ +const char* RecordObject::getRecordTimeField (const char* rec_type) +{ + definition_t* def = getDefinition(rec_type); + if(def == NULL) return NULL; + return def->time_field; +} + +/*---------------------------------------------------------------------------- + * getRecordXField + *----------------------------------------------------------------------------*/ +const char* RecordObject::getRecordXField (const char* rec_type) +{ + definition_t* def = getDefinition(rec_type); + if(def == NULL) return NULL; + return def->x_field; +} + +/*---------------------------------------------------------------------------- + * getRecordYField + *----------------------------------------------------------------------------*/ +const char* RecordObject::getRecordYField (const char* rec_type) +{ + definition_t* def = getDefinition(rec_type); + if(def == NULL) return NULL; + return def->y_field; +} + +/*---------------------------------------------------------------------------- + * getRecordBatchField + *----------------------------------------------------------------------------*/ +const char* RecordObject::getRecordBatchField (const char* rec_type) +{ + definition_t* def = getDefinition(rec_type); + if(def == NULL) return NULL; + return def->batch_field; +} + /*---------------------------------------------------------------------------- * getRecordSize *----------------------------------------------------------------------------*/ @@ -1145,10 +1198,16 @@ unsigned int RecordObject::str2flags (const char* str) for(int i = 0; i < flaglist->length(); i++) { const char* flag = (*flaglist)[i]->c_str(); - if(StringLib::match(flag, "NATIVE")) flags = NATIVE_FLAGS; - else if(StringLib::match(flag, "LE")) flags &= ~BIGENDIAN; - else if(StringLib::match(flag, "BE")) flags |= BIGENDIAN; - else if(StringLib::match(flag, "PTR")) flags |= POINTER; + if(StringLib::match(flag, "NATIVE")) flags = NATIVE_FLAGS; + else if(StringLib::match(flag, "LE")) flags &= ~BIGENDIAN; + else if(StringLib::match(flag, "BE")) flags |= BIGENDIAN; + else if(StringLib::match(flag, "PTR")) flags |= POINTER; + else if(StringLib::match(flag, "AUX")) flags |= AUX; + else if(StringLib::match(flag, "BATCH")) flags |= BATCH; + else if(StringLib::match(flag, "X_COORD")) flags |= X_COORD; + else if(StringLib::match(flag, "Y_COORD")) flags |= Y_COORD; + else if(StringLib::match(flag, "TIME")) flags |= TIME; + else if(StringLib::match(flag, "INDEX")) flags |= INDEX; } delete flaglist; return flags; @@ -1165,10 +1224,13 @@ const char* RecordObject::flags2str (unsigned int flags) if(flags & BIGENDIAN) flagss += "BE"; else flagss += "LE"; - if(flags & POINTER) flagss += "|PTR"; - + if(flags & BATCH) flagss += "|BATCH"; if(flags & AUX) flagss += "|AUX"; + if(flags & X_COORD) flagss += "|X"; + if(flags & Y_COORD) flagss += "|Y"; + if(flags & TIME) flagss += "|T"; + if(flags & INDEX) flagss += "|I"; return StringLib::duplicate(flagss.c_str()); } @@ -1459,7 +1521,7 @@ RecordObject::RecordObject(void) } /*---------------------------------------------------------------------------- - * addDefinition + * getPointedToField * * returns pointer to record definition in rec_def *----------------------------------------------------------------------------*/ @@ -1699,6 +1761,42 @@ RecordObject::recordDefErr_t RecordObject::addField(definition_t* def, const cha return status; } +/*---------------------------------------------------------------------------- +* scanDefinition +*----------------------------------------------------------------------------*/ +void RecordObject::scanDefinition (definition_t* def, const char* field_prefix, const char* rec_type) +{ + /* Get Fields in Record */ + Dictionary* fields = getRecordFields(rec_type); + if(fields == NULL) + { + mlog(CRITICAL, "Unable to scan record type: %s\n", rec_type); + return; + } + + /* Loop Through Fields in Record */ + Dictionary::Iterator field_iter(*fields); + for(int i = 0; i < field_iter.length; i++) + { + Dictionary::kv_t kv = field_iter[i]; + FString field_name("%s%s%s", field_prefix, strlen(field_prefix) == 0 ? "" : ".", kv.key); + const field_t& field = kv.value; + + /* Check for Marked Field */ + if((field.flags & INDEX) && (def->index_field == NULL)) def->index_field = field_name.c_str(true); + if((field.flags & TIME) && (def->time_field == NULL)) def->time_field = field_name.c_str(true); + if((field.flags & X_COORD) && (def->x_field == NULL)) def->x_field = field_name.c_str(true); + if((field.flags & Y_COORD) && (def->y_field == NULL)) def->y_field = field_name.c_str(true); + if((field.flags & BATCH) && (def->batch_field == NULL)) def->batch_field = field_name.c_str(true); + + /* Recurse for User Fields */ + if(field.type == USER) + { + scanDefinition(def, field_name.c_str(), field.exttype); + } + } +} + /*---------------------------------------------------------------------------- * getDefinition *----------------------------------------------------------------------------*/ diff --git a/packages/core/RecordObject.h b/packages/core/RecordObject.h index afc84ff16..88a56e05f 100644 --- a/packages/core/RecordObject.h +++ b/packages/core/RecordObject.h @@ -109,13 +109,17 @@ class RecordObject BIGENDIAN = 0x00000001, POINTER = 0x00000002, BATCH = 0x00000004, // batch record - AUX = 0x00000008 // auxiliary field + AUX = 0x00000008, // auxiliary field + X_COORD = 0x00000010, + Y_COORD = 0x00000020, + TIME = 0x00000040, + INDEX = 0x00000080 } fieldFlags_t; typedef struct { fieldType_t type; // predefined types int32_t offset; // offset in bits into structure - int32_t elements; // size in bits of field + int32_t elements; // number of elements in array const char* exttype; // record type when type=USER unsigned int flags; // see fieldFlags_t } field_t; @@ -250,12 +254,17 @@ class RecordObject static valType_t getValueType (const field_t& field); static recordDefErr_t defineRecord (const char* rec_type, const char* id_field, int data_size, const fieldDef_t* fields, int num_fields, int max_fields=CALC_MAX_FIELDS); static recordDefErr_t defineField (const char* rec_type, const char* field_name, fieldType_t type, int offset, int size, const char* exttype, unsigned int flags=NATIVE_FLAGS); - + /* Utility Static Methods */ static bool isRecord (const char* rec_type); static bool isType (unsigned char* buffer, int size, const char* rec_type); static int getRecords (char*** rec_types); - static const char* getRecordIdField (const char* rec_type); // returns name of field + static const char* getRecordIdField (const char* rec_type); + static const char* getRecordIndexField (const char* rec_type); + static const char* getRecordTimeField (const char* rec_type); + static const char* getRecordXField (const char* rec_type); + static const char* getRecordYField (const char* rec_type); + static const char* getRecordBatchField (const char* rec_type); static int getRecordSize (const char* rec_type); static int getRecordDataSize (const char* rec_type); static int getRecordMaxFields (const char* rec_type); @@ -287,13 +296,23 @@ class RecordObject struct definition_t { const char* type_name; // the name of the type of record - const char* id_field; // field name for id + const char* id_field; // field name for id; used in dispatches + const char* index_field; // field name for index (e.g. extend_id; could be same as id) + const char* time_field; // field name for time + const char* x_field; // field name for x coordinate (e.g. longitude) + const char* y_field; // field name for y coordinate (e.g. latitude) + const char* batch_field; // field name for batch int type_size; // size in bytes of type name string including null termination int data_size; // number of bytes of binary data int record_size; // total size of memory allocated for record Dictionary fields; definition_t(const char* _type_name, const char* _id_field, int _data_size, int _max_fields): + index_field(NULL), + time_field(NULL), + x_field(NULL), + y_field(NULL), + batch_field(NULL), fields(_max_fields) { type_name = StringLib::duplicate(_type_name); type_size = (int)StringLib::size(_type_name) + 1; @@ -302,7 +321,12 @@ class RecordObject record_size = sizeof(rec_hdr_t) + type_size + _data_size; } ~definition_t(void) { delete [] type_name; - delete [] id_field; } + delete [] id_field; + delete [] index_field; + delete [] time_field; + delete [] x_field; + delete [] y_field; + delete [] batch_field; } }; /*-------------------------------------------------------------------- @@ -331,6 +355,7 @@ class RecordObject static field_t getUserField (definition_t* def, const char* field_name, uint32_t parent_flags=NATIVE_FLAGS); static recordDefErr_t addDefinition (definition_t** rec_def, const char* rec_type, const char* id_field, int data_size, const fieldDef_t* fields, int num_fields, int max_fields); static recordDefErr_t addField (definition_t* def, const char* field_name, fieldType_t type, int offset, int elements, const char* exttype, unsigned int flags); + static void scanDefinition (definition_t* def, const char* field_prefix, const char* rec_type); /* Overloaded Methods */ static definition_t* getDefinition (const char* rec_type); diff --git a/plugins/gedi/plugin/Gedi01bReader.cpp b/plugins/gedi/plugin/Gedi01bReader.cpp index 70512eaa7..862d25fc7 100644 --- a/plugins/gedi/plugin/Gedi01bReader.cpp +++ b/plugins/gedi/plugin/Gedi01bReader.cpp @@ -47,10 +47,10 @@ const char* Gedi01bReader::fpRecType = "gedi01brec.footprint"; const RecordObject::fieldDef_t Gedi01bReader::fpRecDef[] = { - {"shot_number", RecordObject::UINT64, offsetof(g01b_footprint_t, shot_number), 1, NULL, NATIVE_FLAGS}, - {"time", RecordObject::TIME8, offsetof(g01b_footprint_t, time_ns), 1, NULL, NATIVE_FLAGS}, - {"latitude", RecordObject::DOUBLE, offsetof(g01b_footprint_t, latitude), 1, NULL, NATIVE_FLAGS}, - {"longitude", RecordObject::DOUBLE, offsetof(g01b_footprint_t, longitude), 1, NULL, NATIVE_FLAGS}, + {"shot_number", RecordObject::UINT64, offsetof(g01b_footprint_t, shot_number), 1, NULL, NATIVE_FLAGS | RecordObject::INDEX}, + {"time", RecordObject::TIME8, offsetof(g01b_footprint_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, + {"latitude", RecordObject::DOUBLE, offsetof(g01b_footprint_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, + {"longitude", RecordObject::DOUBLE, offsetof(g01b_footprint_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, {"elevation_start", RecordObject::DOUBLE, offsetof(g01b_footprint_t, elevation_start), 1, NULL, NATIVE_FLAGS}, {"elevation_stop", RecordObject::DOUBLE, offsetof(g01b_footprint_t, elevation_stop), 1, NULL, NATIVE_FLAGS}, {"solar_elevation", RecordObject::DOUBLE, offsetof(g01b_footprint_t, solar_elevation), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/gedi/plugin/Gedi02aReader.cpp b/plugins/gedi/plugin/Gedi02aReader.cpp index 5e9a4e794..0ea25c24a 100644 --- a/plugins/gedi/plugin/Gedi02aReader.cpp +++ b/plugins/gedi/plugin/Gedi02aReader.cpp @@ -47,10 +47,10 @@ const char* Gedi02aReader::fpRecType = "gedi02arec.footprint"; const RecordObject::fieldDef_t Gedi02aReader::fpRecDef[] = { - {"shot_number", RecordObject::UINT64, offsetof(g02a_footprint_t, shot_number), 1, NULL, NATIVE_FLAGS}, - {"time", RecordObject::TIME8, offsetof(g02a_footprint_t, time_ns), 1, NULL, NATIVE_FLAGS}, - {"latitude", RecordObject::DOUBLE, offsetof(g02a_footprint_t, latitude), 1, NULL, NATIVE_FLAGS}, - {"longitude", RecordObject::DOUBLE, offsetof(g02a_footprint_t, longitude), 1, NULL, NATIVE_FLAGS}, + {"shot_number", RecordObject::UINT64, offsetof(g02a_footprint_t, shot_number), 1, NULL, NATIVE_FLAGS | RecordObject::INDEX}, + {"time", RecordObject::TIME8, offsetof(g02a_footprint_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, + {"latitude", RecordObject::DOUBLE, offsetof(g02a_footprint_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, + {"longitude", RecordObject::DOUBLE, offsetof(g02a_footprint_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, {"elevation_lm", RecordObject::FLOAT, offsetof(g02a_footprint_t, elevation_lowestmode), 1, NULL, NATIVE_FLAGS}, {"elevation_hr", RecordObject::FLOAT, offsetof(g02a_footprint_t, elevation_highestreturn), 1, NULL, NATIVE_FLAGS}, {"solar_elevation", RecordObject::FLOAT, offsetof(g02a_footprint_t, solar_elevation), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/gedi/plugin/Gedi04aReader.cpp b/plugins/gedi/plugin/Gedi04aReader.cpp index 73915ea34..e5e9fb480 100644 --- a/plugins/gedi/plugin/Gedi04aReader.cpp +++ b/plugins/gedi/plugin/Gedi04aReader.cpp @@ -47,10 +47,10 @@ const char* Gedi04aReader::fpRecType = "gedi04arec.footprint"; const RecordObject::fieldDef_t Gedi04aReader::fpRecDef[] = { - {"shot_number", RecordObject::UINT64, offsetof(g04a_footprint_t, shot_number), 1, NULL, NATIVE_FLAGS}, - {"time", RecordObject::TIME8, offsetof(g04a_footprint_t, time_ns), 1, NULL, NATIVE_FLAGS}, - {"latitude", RecordObject::DOUBLE, offsetof(g04a_footprint_t, latitude), 1, NULL, NATIVE_FLAGS}, - {"longitude", RecordObject::DOUBLE, offsetof(g04a_footprint_t, longitude), 1, NULL, NATIVE_FLAGS}, + {"shot_number", RecordObject::UINT64, offsetof(g04a_footprint_t, shot_number), 1, NULL, NATIVE_FLAGS | RecordObject::INDEX}, + {"time", RecordObject::TIME8, offsetof(g04a_footprint_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, + {"latitude", RecordObject::DOUBLE, offsetof(g04a_footprint_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, + {"longitude", RecordObject::DOUBLE, offsetof(g04a_footprint_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, {"agbd", RecordObject::FLOAT, offsetof(g04a_footprint_t, agbd), 1, NULL, NATIVE_FLAGS}, {"elevation", RecordObject::FLOAT, offsetof(g04a_footprint_t, elevation), 1, NULL, NATIVE_FLAGS}, {"solar_elevation", RecordObject::FLOAT, offsetof(g04a_footprint_t, solar_elevation), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/icesat2/plugin/Atl03Reader.cpp b/plugins/icesat2/plugin/Atl03Reader.cpp index 872f8d8be..488e4a244 100644 --- a/plugins/icesat2/plugin/Atl03Reader.cpp +++ b/plugins/icesat2/plugin/Atl03Reader.cpp @@ -47,9 +47,9 @@ const char* Atl03Reader::phRecType = "atl03rec.photons"; const RecordObject::fieldDef_t Atl03Reader::phRecDef[] = { - {"time", RecordObject::TIME8, offsetof(photon_t, time_ns), 1, NULL, NATIVE_FLAGS}, - {"latitude", RecordObject::DOUBLE, offsetof(photon_t, latitude), 1, NULL, NATIVE_FLAGS}, - {"longitude", RecordObject::DOUBLE, offsetof(photon_t, longitude), 1, NULL, NATIVE_FLAGS}, + {"time", RecordObject::TIME8, offsetof(photon_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, + {"latitude", RecordObject::DOUBLE, offsetof(photon_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, + {"longitude", RecordObject::DOUBLE, offsetof(photon_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, {"x_atc", RecordObject::FLOAT, offsetof(photon_t, x_atc), 1, NULL, NATIVE_FLAGS}, {"y_atc", RecordObject::FLOAT, offsetof(photon_t, y_atc), 1, NULL, NATIVE_FLAGS}, {"height", RecordObject::FLOAT, offsetof(photon_t, height), 1, NULL, NATIVE_FLAGS}, @@ -74,7 +74,7 @@ const RecordObject::fieldDef_t Atl03Reader::exRecDef[] = { {"segment_dist", RecordObject::DOUBLE, offsetof(extent_t, segment_distance), 1, NULL, NATIVE_FLAGS}, // distance from equator {"background_rate", RecordObject::DOUBLE, offsetof(extent_t, background_rate), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"solar_elevation", RecordObject::FLOAT, offsetof(extent_t, solar_elevation), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, - {"extent_id", RecordObject::UINT64, offsetof(extent_t, extent_id), 1, NULL, NATIVE_FLAGS}, + {"extent_id", RecordObject::UINT64, offsetof(extent_t, extent_id), 1, NULL, NATIVE_FLAGS | RecordObject::INDEX}, {"photons", RecordObject::USER, offsetof(extent_t, photons), 0, phRecType, NATIVE_FLAGS | RecordObject::BATCH} // variable length }; diff --git a/plugins/icesat2/plugin/Atl06Dispatch.cpp b/plugins/icesat2/plugin/Atl06Dispatch.cpp index f87e95667..ea9f40c52 100644 --- a/plugins/icesat2/plugin/Atl06Dispatch.cpp +++ b/plugins/icesat2/plugin/Atl06Dispatch.cpp @@ -85,7 +85,7 @@ const double Atl06Dispatch::SIGMA_XMIT = 0.00000000068; // seconds const char* Atl06Dispatch::elRecType = "atl06rec.elevation"; // extended elevation measurement record const RecordObject::fieldDef_t Atl06Dispatch::elRecDef[] = { - {"extent_id", RecordObject::UINT64, offsetof(elevation_t, extent_id), 1, NULL, NATIVE_FLAGS}, + {"extent_id", RecordObject::UINT64, offsetof(elevation_t, extent_id), 1, NULL, NATIVE_FLAGS | RecordObject::INDEX}, {"segment_id", RecordObject::UINT32, offsetof(elevation_t, segment_id), 1, NULL, NATIVE_FLAGS}, {"n_fit_photons", RecordObject::INT32, offsetof(elevation_t, photon_count), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"pflags", RecordObject::UINT16, offsetof(elevation_t, pflags), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, @@ -94,9 +94,9 @@ const RecordObject::fieldDef_t Atl06Dispatch::elRecDef[] = { {"region", RecordObject::UINT8, offsetof(elevation_t, region), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"spot", RecordObject::UINT8, offsetof(elevation_t, spot), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"gt", RecordObject::UINT8, offsetof(elevation_t, gt), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, - {"time", RecordObject::TIME8, offsetof(elevation_t, time_ns), 1, NULL, NATIVE_FLAGS}, - {"latitude", RecordObject::DOUBLE, offsetof(elevation_t, latitude), 1, NULL, NATIVE_FLAGS}, - {"longitude", RecordObject::DOUBLE, offsetof(elevation_t, longitude), 1, NULL, NATIVE_FLAGS}, + {"time", RecordObject::TIME8, offsetof(elevation_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, + {"latitude", RecordObject::DOUBLE, offsetof(elevation_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, + {"longitude", RecordObject::DOUBLE, offsetof(elevation_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, {"h_mean", RecordObject::DOUBLE, offsetof(elevation_t, h_mean), 1, NULL, NATIVE_FLAGS}, {"dh_fit_dx", RecordObject::FLOAT, offsetof(elevation_t, dh_fit_dx), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"x_atc", RecordObject::FLOAT, offsetof(elevation_t, x_atc), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/icesat2/plugin/Atl06Reader.cpp b/plugins/icesat2/plugin/Atl06Reader.cpp index ab88ff996..614f2a357 100644 --- a/plugins/icesat2/plugin/Atl06Reader.cpp +++ b/plugins/icesat2/plugin/Atl06Reader.cpp @@ -51,17 +51,17 @@ using std::numeric_limits; const char* Atl06Reader::elRecType = "atl06srec.elevation"; const RecordObject::fieldDef_t Atl06Reader::elRecDef[] = { - {"extent_id", RecordObject::UINT64, offsetof(elevation_t, extent_id), 1, NULL, NATIVE_FLAGS}, + {"extent_id", RecordObject::UINT64, offsetof(elevation_t, extent_id), 1, NULL, NATIVE_FLAGS | RecordObject::INDEX}, {"rgt", RecordObject::UINT16, offsetof(elevation_t, rgt), 1, NULL, NATIVE_FLAGS}, {"cycle", RecordObject::UINT16, offsetof(elevation_t, cycle), 1, NULL, NATIVE_FLAGS}, {"spot", RecordObject::UINT8, offsetof(elevation_t, spot), 1, NULL, NATIVE_FLAGS}, {"gt", RecordObject::UINT8, offsetof(elevation_t, gt), 1, NULL, NATIVE_FLAGS}, // land_ice_segments - {"time", RecordObject::TIME8, offsetof(elevation_t, time_ns), 1, NULL, NATIVE_FLAGS}, + {"time", RecordObject::TIME8, offsetof(elevation_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, {"h_li", RecordObject::FLOAT, offsetof(elevation_t, h_li), 1, NULL, NATIVE_FLAGS}, {"h_li_sigma", RecordObject::FLOAT, offsetof(elevation_t, h_li_sigma), 1, NULL, NATIVE_FLAGS}, - {"latitude", RecordObject::DOUBLE, offsetof(elevation_t, latitude), 1, NULL, NATIVE_FLAGS}, - {"longitude", RecordObject::DOUBLE, offsetof(elevation_t, longitude), 1, NULL, NATIVE_FLAGS}, + {"latitude", RecordObject::DOUBLE, offsetof(elevation_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, + {"longitude", RecordObject::DOUBLE, offsetof(elevation_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, {"atl06_quality_summary", RecordObject::INT8, offsetof(elevation_t, atl06_quality_summary), 1, NULL, NATIVE_FLAGS}, {"segment_id", RecordObject::UINT32, offsetof(elevation_t, segment_id), 1, NULL, NATIVE_FLAGS}, {"sigma_geo_h", RecordObject::FLOAT, offsetof(elevation_t, sigma_geo_h), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/icesat2/plugin/Atl08Dispatch.cpp b/plugins/icesat2/plugin/Atl08Dispatch.cpp index 9c2b2ed7b..2489e42d5 100644 --- a/plugins/icesat2/plugin/Atl08Dispatch.cpp +++ b/plugins/icesat2/plugin/Atl08Dispatch.cpp @@ -51,7 +51,7 @@ using std::numeric_limits; const char* Atl08Dispatch::vegRecType = "atl08rec.vegetation"; const RecordObject::fieldDef_t Atl08Dispatch::vegRecDef[] = { - {"extent_id", RecordObject::UINT64, offsetof(vegetation_t, extent_id), 1, NULL, NATIVE_FLAGS}, + {"extent_id", RecordObject::UINT64, offsetof(vegetation_t, extent_id), 1, NULL, NATIVE_FLAGS | RecordObject::INDEX}, {"segment_id", RecordObject::UINT32, offsetof(vegetation_t, segment_id), 1, NULL, NATIVE_FLAGS}, {"rgt", RecordObject::UINT16, offsetof(vegetation_t, rgt), 1, NULL, NATIVE_FLAGS}, {"cycle", RecordObject::UINT16, offsetof(vegetation_t, cycle), 1, NULL, NATIVE_FLAGS}, @@ -62,9 +62,9 @@ const RecordObject::fieldDef_t Atl08Dispatch::vegRecDef[] = { {"veg_ph_count", RecordObject::UINT32, offsetof(vegetation_t, vegetation_photon_count),1, NULL, NATIVE_FLAGS}, {"landcover", RecordObject::UINT8, offsetof(vegetation_t, landcover), 1, NULL, NATIVE_FLAGS}, {"snowcover", RecordObject::UINT8, offsetof(vegetation_t, snowcover), 1, NULL, NATIVE_FLAGS}, - {"time", RecordObject::TIME8, offsetof(vegetation_t, time_ns), 1, NULL, NATIVE_FLAGS}, - {"latitude", RecordObject::DOUBLE, offsetof(vegetation_t, latitude), 1, NULL, NATIVE_FLAGS}, - {"longitude", RecordObject::DOUBLE, offsetof(vegetation_t, longitude), 1, NULL, NATIVE_FLAGS}, + {"time", RecordObject::TIME8, offsetof(vegetation_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, + {"latitude", RecordObject::DOUBLE, offsetof(vegetation_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, + {"longitude", RecordObject::DOUBLE, offsetof(vegetation_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, {"x_atc", RecordObject::DOUBLE, offsetof(vegetation_t, x_atc), 1, NULL, NATIVE_FLAGS}, {"solar_elevation", RecordObject::FLOAT, offsetof(vegetation_t, solar_elevation), 1, NULL, NATIVE_FLAGS}, {"h_te_median", RecordObject::FLOAT, offsetof(vegetation_t, h_te_median), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/swot/plugin/SwotL2Reader.cpp b/plugins/swot/plugin/SwotL2Reader.cpp index d451d7b82..4ec9a34d2 100644 --- a/plugins/swot/plugin/SwotL2Reader.cpp +++ b/plugins/swot/plugin/SwotL2Reader.cpp @@ -73,8 +73,8 @@ const RecordObject::fieldDef_t SwotL2Reader::varRecDef[] = { const char* SwotL2Reader::scanRecType = "swotl2geo.scan"; const RecordObject::fieldDef_t SwotL2Reader::scanRecDef[] = { {"scan_id", RecordObject::UINT64, offsetof(scan_rec_t, scan_id), 1, NULL, NATIVE_FLAGS}, - {"latitude", RecordObject::DOUBLE, offsetof(scan_rec_t, latitude), 1, NULL, NATIVE_FLAGS}, - {"longitude", RecordObject::DOUBLE, offsetof(scan_rec_t, longitude), 1, NULL, NATIVE_FLAGS} + {"latitude", RecordObject::DOUBLE, offsetof(scan_rec_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, + {"longitude", RecordObject::DOUBLE, offsetof(scan_rec_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD} }; const char* SwotL2Reader::geoRecType = "swotl2geo"; From e055bc6e419f13c0f95ab0ba60ada8897bfa2aa9 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 29 Feb 2024 21:28:52 +0000 Subject: [PATCH 26/43] metadata for records consolidated in the record definition --- packages/arrow/ArrowImpl.cpp | 24 +----- packages/arrow/ArrowImpl.h | 2 - packages/arrow/ParquetBuilder.cpp | 95 ++++++++++++------------ packages/arrow/ParquetBuilder.h | 4 +- packages/core/RecordObject.cpp | 41 +++++++--- packages/core/RecordObject.h | 56 +++++++++----- packages/geo/RasterSampler.cpp | 90 +++++++--------------- packages/geo/RasterSampler.h | 4 +- plugins/gedi/endpoints/gedi01b.lua | 5 -- plugins/gedi/endpoints/gedi01bp.lua | 2 +- plugins/gedi/endpoints/gedi02a.lua | 5 -- plugins/gedi/endpoints/gedi02ap.lua | 2 +- plugins/gedi/endpoints/gedi04a.lua | 5 -- plugins/gedi/endpoints/gedi04ap.lua | 2 +- plugins/gedi/plugin/Gedi01bReader.cpp | 2 +- plugins/gedi/plugin/Gedi02aReader.cpp | 2 +- plugins/gedi/plugin/Gedi04aReader.cpp | 2 +- plugins/icesat2/endpoints/atl03s.lua | 5 -- plugins/icesat2/endpoints/atl03sp.lua | 2 +- plugins/icesat2/endpoints/atl06.lua | 5 -- plugins/icesat2/endpoints/atl06p.lua | 2 +- plugins/icesat2/endpoints/atl06s.lua | 5 -- plugins/icesat2/endpoints/atl06sp.lua | 2 +- plugins/icesat2/endpoints/atl08.lua | 5 -- plugins/icesat2/endpoints/atl08p.lua | 2 +- plugins/icesat2/plugin/Atl03Reader.cpp | 2 +- plugins/icesat2/plugin/Atl06Dispatch.cpp | 2 +- plugins/icesat2/plugin/Atl06Reader.cpp | 2 +- plugins/icesat2/plugin/Atl08Dispatch.cpp | 2 +- plugins/swot/endpoints/swotl2.lua | 3 - plugins/swot/endpoints/swotl2p.lua | 2 +- scripts/extensions/georesource.lua | 3 +- scripts/extensions/proxy.lua | 4 +- 33 files changed, 165 insertions(+), 226 deletions(-) diff --git a/packages/arrow/ArrowImpl.cpp b/packages/arrow/ArrowImpl.cpp index 93b6e1fde..2f67a902a 100644 --- a/packages/arrow/ArrowImpl.cpp +++ b/packages/arrow/ArrowImpl.cpp @@ -54,7 +54,6 @@ ArrowImpl::ArrowImpl (ParquetBuilder* _builder): schema(NULL), fieldList(LIST_BLOCK_SIZE), fieldIterator(NULL), - batchRecType(NULL), firstTime(true) { /* Build Field List and Iterator */ @@ -69,15 +68,6 @@ ArrowImpl::ArrowImpl (ParquetBuilder* _builder): ArrowImpl::~ArrowImpl (void) { delete fieldIterator; - delete [] batchRecType; -} - -/*---------------------------------------------------------------------------- -* getBatchRecType -*----------------------------------------------------------------------------*/ -const char* ArrowImpl::getBatchRecType (void) -{ - return batchRecType; } /*---------------------------------------------------------------------------- @@ -208,22 +198,14 @@ bool ArrowImpl::buildFieldList (const char* rec_type, int offset, int flags) /* Check for Geometry Columns */ if(parquetBuilder->getAsGeo()) { - -// TODO: this could just check for the x and y flags - if(field.offset == parquetBuilder->getXField().offset || field.offset == parquetBuilder->getYField().offset) + /* skip over source columns for geometry as they will be added + * separately as a part of the dedicated geometry column */ + if(field.flags & (RecordObject::X_COORD | RecordObject::Y_COORD)) { - /* skip over source columns for geometry as they will be added - * separately as a part of the dedicated geometry column */ continue; } } - /* Check for Batch Record Type */ - if((batchRecType == NULL) && (field.flags & RecordObject::BATCH)) - { - batchRecType = StringLib::duplicate(field.exttype); - } - /* Add to Schema */ if(field.elements == 1) { diff --git a/packages/arrow/ArrowImpl.h b/packages/arrow/ArrowImpl.h index b5c51aa12..a387056ce 100644 --- a/packages/arrow/ArrowImpl.h +++ b/packages/arrow/ArrowImpl.h @@ -71,7 +71,6 @@ class ArrowImpl explicit ArrowImpl (ParquetBuilder* _builder); ~ArrowImpl (void); - const char* getBatchRecType (void); bool processRecordBatch (Ordering& record_batch, int num_rows, int batch_row_size_bits, @@ -110,7 +109,6 @@ class ArrowImpl vector> fieldVector; field_list_t fieldList; field_iterator_t* fieldIterator; - const char* batchRecType; bool firstTime; /*-------------------------------------------------------------------- diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index ac1a744f4..98cfc6ff0 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -82,9 +82,6 @@ const char* ParquetBuilder::TMP_FILE_PREFIX = "/tmp/"; int ParquetBuilder::luaCreate (lua_State* L) { ArrowParms* _parms = NULL; - geo_data_t geo; - geo.x_key = NULL; - geo.y_key = NULL; try { @@ -94,43 +91,13 @@ int ParquetBuilder::luaCreate (lua_State* L) const char* inq_name = getLuaString(L, 3); const char* rec_type = getLuaString(L, 4); const char* id = getLuaString(L, 5); - const char* x_key = getLuaString(L, 6, true, NULL); - const char* y_key = getLuaString(L, 7, true, NULL); - const char* index_key = getLuaString(L, 8, true, NULL); - - /* Build Geometry Fields */ - geo.as_geo = _parms->as_geo; - if(geo.as_geo && (x_key != NULL) && (y_key != NULL)) - { - geo.x_field = RecordObject::getDefinedField(rec_type, x_key); - if(geo.x_field.type == RecordObject::INVALID_FIELD) - { - throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to extract x field [%s] from record type <%s>", x_key, rec_type); - } - - geo.y_field = RecordObject::getDefinedField(rec_type, y_key); - if(geo.y_field.type == RecordObject::INVALID_FIELD) - { - throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to extract y field [%s] from record type <%s>", y_key, rec_type); - } - - geo.x_key = StringLib::duplicate(x_key); - geo.y_key = StringLib::duplicate(y_key); - } - else - { - /* Unable to Create GeoParquet */ - geo.as_geo = false; - } /* Create Dispatch */ - return createLuaObject(L, new ParquetBuilder(L, _parms, outq_name, inq_name, rec_type, id, geo, index_key)); + return createLuaObject(L, new ParquetBuilder(L, _parms, outq_name, inq_name, rec_type, id)); } catch(const RunTimeException& e) { if(_parms) _parms->releaseLuaObject(); - delete [] geo.x_key; - delete [] geo.y_key; mlog(e.level(), "Error creating %s: %s", LUA_META_NAME, e.what()); return returnLuaStatus(L, false); } @@ -210,11 +177,9 @@ RecordObject::field_t& ParquetBuilder::getYField (void) *----------------------------------------------------------------------------*/ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, const char* outq_name, const char* inq_name, - const char* rec_type, const char* id, const geo_data_t& geo, const char* index_key): + const char* rec_type, const char* id): LuaObject(L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), - parms(_parms), - recType(StringLib::duplicate(rec_type)), - geoData(geo) + parms(_parms) { assert(_parms); assert(outq_name); @@ -222,6 +187,38 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, assert(rec_type); assert(id); + /* Get Record Meta Data */ + RecordObject::meta_t* rec_meta = RecordObject::getRecordMetaFields(rec_type); + if(rec_meta == NULL) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to get meta data for %s", rec_type); + } + + /* Build Geometry Fields */ + geoData.as_geo = parms->as_geo; + if(geoData.as_geo) + { + /* Check if Record has Geospatial Fields */ + if((rec_meta->x_field == NULL) || (rec_meta->y_field == NULL)) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to get x and y coordinates for %s", rec_type); + } + + /* Get X Field */ + geoData.x_field = RecordObject::getDefinedField(rec_type, rec_meta->x_field); + if(geoData.x_field.type == RecordObject::INVALID_FIELD) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to extract x field [%s] from record type <%s>", rec_meta->x_field, rec_type); + } + + /* Get Y Field */ + geoData.y_field = RecordObject::getDefinedField(rec_type, rec_meta->y_field); + if(geoData.y_field.type == RecordObject::INVALID_FIELD) + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to extract y field [%s] from record type <%s>", rec_meta->y_field, rec_type); + } + } + /* Get Path */ if(parms->asset_name) { @@ -252,9 +249,19 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, outputPath = StringLib::duplicate(parms->path); } + /* + * NO THROWING BEYOND THIS POINT + */ + + /* Set Record Type */ + recType = StringLib::duplicate(rec_type); + + /* Save Index Key */ + indexKey = StringLib::duplicate(rec_meta->index_field); + printf("INDEX KEY: %s\n", indexKey); + /* Get Row Size */ - const char* batch_rec_field_name = RecordObject::getRecordBatchField(recType); - RecordObject::field_t batch_rec_field = RecordObject::getDefinedField(recType, batch_rec_field_name); + RecordObject::field_t batch_rec_field = RecordObject::getDefinedField(recType, rec_meta->batch_field); if(batch_rec_field.type == RecordObject::INVALID_FIELD) batchRowSizeBytes = 0; else batchRowSizeBytes = RecordObject::getRecordDataSize(batch_rec_field.exttype); rowSizeBytes = RecordObject::getRecordDataSize(recType) + batchRowSizeBytes; @@ -269,9 +276,6 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, FString tmp_file("%s%s.parquet", TMP_FILE_PREFIX, id); fileName = tmp_file.c_str(true); - /* Save Index Key */ - indexKey = StringLib::duplicate(index_key); - /* Allocate Implementation */ impl = new ArrowImpl(this); @@ -295,11 +299,6 @@ ParquetBuilder::~ParquetBuilder(void) delete outQ; delete inQ; delete impl; - if(geoData.as_geo) - { - delete [] geoData.x_key; - delete [] geoData.y_key; - } } /*---------------------------------------------------------------------------- diff --git a/packages/arrow/ParquetBuilder.h b/packages/arrow/ParquetBuilder.h index 17c3cf5b1..b81a80905 100644 --- a/packages/arrow/ParquetBuilder.h +++ b/packages/arrow/ParquetBuilder.h @@ -97,8 +97,6 @@ class ParquetBuilder: public LuaObject typedef struct { bool as_geo; - const char* x_key; - const char* y_key; RecordObject::field_t x_field; RecordObject::field_t y_field; } geo_data_t; @@ -168,7 +166,7 @@ class ParquetBuilder: public LuaObject ParquetBuilder (lua_State* L, ArrowParms* parms, const char* outq_name, const char* inq_name, - const char* rec_type, const char* id, const geo_data_t& geo, const char* index_key); + const char* rec_type, const char* id); ~ParquetBuilder (void); static void* builderThread (void* parm); diff --git a/packages/core/RecordObject.cpp b/packages/core/RecordObject.cpp index ffc6df5f9..300e52df7 100644 --- a/packages/core/RecordObject.cpp +++ b/packages/core/RecordObject.cpp @@ -1050,7 +1050,7 @@ const char* RecordObject::getRecordIndexField (const char* rec_type) { definition_t* def = getDefinition(rec_type); if(def == NULL) return NULL; - return def->index_field; + return def->meta.index_field; } /*---------------------------------------------------------------------------- @@ -1060,7 +1060,7 @@ const char* RecordObject::getRecordTimeField (const char* rec_type) { definition_t* def = getDefinition(rec_type); if(def == NULL) return NULL; - return def->time_field; + return def->meta.time_field; } /*---------------------------------------------------------------------------- @@ -1070,7 +1070,7 @@ const char* RecordObject::getRecordXField (const char* rec_type) { definition_t* def = getDefinition(rec_type); if(def == NULL) return NULL; - return def->x_field; + return def->meta.x_field; } /*---------------------------------------------------------------------------- @@ -1080,7 +1080,17 @@ const char* RecordObject::getRecordYField (const char* rec_type) { definition_t* def = getDefinition(rec_type); if(def == NULL) return NULL; - return def->y_field; + return def->meta.y_field; +} + +/*---------------------------------------------------------------------------- + * getRecordZField + *----------------------------------------------------------------------------*/ +const char* RecordObject::getRecordZField (const char* rec_type) +{ + definition_t* def = getDefinition(rec_type); + if(def == NULL) return NULL; + return def->meta.z_field; } /*---------------------------------------------------------------------------- @@ -1090,7 +1100,17 @@ const char* RecordObject::getRecordBatchField (const char* rec_type) { definition_t* def = getDefinition(rec_type); if(def == NULL) return NULL; - return def->batch_field; + return def->meta.batch_field; +} + +/*---------------------------------------------------------------------------- + * getRecordMetaFields + *----------------------------------------------------------------------------*/ +RecordObject::meta_t* RecordObject::getRecordMetaFields (const char* rec_type) +{ + definition_t* def = getDefinition(rec_type); + if(def == NULL) return NULL; + return &(def->meta); } /*---------------------------------------------------------------------------- @@ -1783,11 +1803,12 @@ void RecordObject::scanDefinition (definition_t* def, const char* field_prefix, const field_t& field = kv.value; /* Check for Marked Field */ - if((field.flags & INDEX) && (def->index_field == NULL)) def->index_field = field_name.c_str(true); - if((field.flags & TIME) && (def->time_field == NULL)) def->time_field = field_name.c_str(true); - if((field.flags & X_COORD) && (def->x_field == NULL)) def->x_field = field_name.c_str(true); - if((field.flags & Y_COORD) && (def->y_field == NULL)) def->y_field = field_name.c_str(true); - if((field.flags & BATCH) && (def->batch_field == NULL)) def->batch_field = field_name.c_str(true); + if((field.flags & INDEX) && (def->meta.index_field == NULL)) def->meta.index_field = field_name.c_str(true); + if((field.flags & TIME) && (def->meta.time_field == NULL)) def->meta.time_field = field_name.c_str(true); + if((field.flags & X_COORD) && (def->meta.x_field == NULL)) def->meta.x_field = field_name.c_str(true); + if((field.flags & Y_COORD) && (def->meta.y_field == NULL)) def->meta.y_field = field_name.c_str(true); + if((field.flags & Z_COORD) && (def->meta.z_field == NULL)) def->meta.z_field = field_name.c_str(true); + if((field.flags & BATCH) && (def->meta.batch_field == NULL)) def->meta.batch_field = field_name.c_str(true); /* Recurse for User Fields */ if(field.type == USER) diff --git a/packages/core/RecordObject.h b/packages/core/RecordObject.h index 88a56e05f..886c09a32 100644 --- a/packages/core/RecordObject.h +++ b/packages/core/RecordObject.h @@ -108,12 +108,13 @@ class RecordObject typedef enum { BIGENDIAN = 0x00000001, POINTER = 0x00000002, - BATCH = 0x00000004, // batch record - AUX = 0x00000008, // auxiliary field + AUX = 0x00000004, // auxiliary field + BATCH = 0x00000008, // batch record X_COORD = 0x00000010, Y_COORD = 0x00000020, - TIME = 0x00000040, - INDEX = 0x00000080 + Z_COORD = 0x00000040, + TIME = 0x00000080, + INDEX = 0x00000100 } fieldFlags_t; typedef struct { @@ -161,6 +162,33 @@ class RecordObject uint32_t data_size; } rec_hdr_t; + struct meta_t + { + const char* index_field; // field name for index (e.g. extend_id; could be same as id) + const char* time_field; // field name for time + const char* x_field; // field name for x coordinate (e.g. longitude) + const char* y_field; // field name for y coordinate (e.g. latitude) + const char* z_field; + const char* batch_field; // field name for batch + + meta_t(void): + index_field(NULL), + time_field(NULL), + x_field(NULL), + y_field(NULL), + z_field(NULL), + batch_field(NULL) {} + ~meta_t(void) + { + delete [] index_field; + delete [] time_field; + delete [] x_field; + delete [] y_field; + delete [] z_field; + delete [] batch_field; + } + }; + /*-------------------------------------------------------------------- * Constants *--------------------------------------------------------------------*/ @@ -264,7 +292,9 @@ class RecordObject static const char* getRecordTimeField (const char* rec_type); static const char* getRecordXField (const char* rec_type); static const char* getRecordYField (const char* rec_type); + static const char* getRecordZField (const char* rec_type); static const char* getRecordBatchField (const char* rec_type); + static meta_t* getRecordMetaFields (const char* rec_type); static int getRecordSize (const char* rec_type); static int getRecordDataSize (const char* rec_type); static int getRecordMaxFields (const char* rec_type); @@ -297,22 +327,13 @@ class RecordObject { const char* type_name; // the name of the type of record const char* id_field; // field name for id; used in dispatches - const char* index_field; // field name for index (e.g. extend_id; could be same as id) - const char* time_field; // field name for time - const char* x_field; // field name for x coordinate (e.g. longitude) - const char* y_field; // field name for y coordinate (e.g. latitude) - const char* batch_field; // field name for batch + meta_t meta; // meta fields populated by the scanDefinition function int type_size; // size in bytes of type name string including null termination int data_size; // number of bytes of binary data int record_size; // total size of memory allocated for record Dictionary fields; definition_t(const char* _type_name, const char* _id_field, int _data_size, int _max_fields): - index_field(NULL), - time_field(NULL), - x_field(NULL), - y_field(NULL), - batch_field(NULL), fields(_max_fields) { type_name = StringLib::duplicate(_type_name); type_size = (int)StringLib::size(_type_name) + 1; @@ -321,12 +342,7 @@ class RecordObject record_size = sizeof(rec_hdr_t) + type_size + _data_size; } ~definition_t(void) { delete [] type_name; - delete [] id_field; - delete [] index_field; - delete [] time_field; - delete [] x_field; - delete [] y_field; - delete [] batch_field; } + delete [] id_field; } }; /*-------------------------------------------------------------------- diff --git a/packages/geo/RasterSampler.cpp b/packages/geo/RasterSampler.cpp index 7f731b262..2db790fd1 100644 --- a/packages/geo/RasterSampler.cpp +++ b/packages/geo/RasterSampler.cpp @@ -97,7 +97,7 @@ const RecordObject::fieldDef_t RasterSampler::fileIdRecDef[] = { ******************************************************************************/ /*---------------------------------------------------------------------------- - * luaCreate - :sampler(, , , , , , ) + * luaCreate - :sampler(, , , , , ) *----------------------------------------------------------------------------*/ int RasterSampler::luaCreate (lua_State* L) { @@ -109,14 +109,10 @@ int RasterSampler::luaCreate (lua_State* L) const char* raster_key = getLuaString(L, 2); const char* outq_name = getLuaString(L, 3); const char* rec_type = getLuaString(L, 4); - const char* index_key = getLuaString(L, 5); - const char* lon_key = getLuaString(L, 6); - const char* lat_key = getLuaString(L, 7); - const char* time_key = getLuaString(L, 8, true, NULL); - const char* height_key = getLuaString(L, 9, true, NULL); + bool use_time = getLuaBoolean(L, 5, true, false); /* Create Dispatch */ - return createLuaObject(L, new RasterSampler(L, _raster, raster_key, outq_name, rec_type, index_key, lon_key, lat_key, time_key, height_key)); + return createLuaObject(L, new RasterSampler(L, _raster, raster_key, outq_name, rec_type, use_time)); } catch(const RunTimeException& e) { @@ -153,83 +149,53 @@ void RasterSampler::deinit (void) * Constructor *----------------------------------------------------------------------------*/ RasterSampler::RasterSampler (lua_State* L, RasterObject* _raster, const char* raster_key, - const char* outq_name, const char* rec_type, - const char* index_key, const char* lon_key, const char* lat_key, - const char* time_key, const char* height_key): + const char* outq_name, const char* rec_type, bool use_time): DispatchObject(L, LUA_META_NAME, LUA_META_TABLE) { assert(_raster); assert(outq_name); - assert(lon_key); - assert(lat_key); - /* Initialize Class Data */ - raster = _raster; - rasterKey = StringLib::duplicate(raster_key); - outQ = new Publisher(outq_name); + /* Get Record Meta Data */ + RecordObject::meta_t* rec_meta = RecordObject::getRecordMetaFields(rec_type); + if(rec_meta == NULL) throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to get meta data for %s", rec_type); /* Determine Record Batch Size */ - batchRecordSizeBytes = 0; - Dictionary* fields = RecordObject::getRecordFields(rec_type); - Dictionary::Iterator field_iter(*fields); - for(int i = 0; i < field_iter.length; i++) - { - if(field_iter[i].value.flags & RecordObject::BATCH) - { - batchRecordSizeBytes = RecordObject::getRecordDataSize(field_iter[i].value.exttype); - break; - } - } + RecordObject::field_t batch_rec_field = RecordObject::getDefinedField(rec_type, rec_meta->batch_field); + if(batch_rec_field.type == RecordObject::INVALID_FIELD) throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to get batch size <%s> for %s", rec_meta->batch_field, rec_type); + batchRecordSizeBytes = RecordObject::getRecordDataSize(batch_rec_field.exttype); /* Determine Record Size */ recordSizeBytes = RecordObject::getRecordDataSize(rec_type) + batchRecordSizeBytes; - if(recordSizeBytes <= 0) - { - mlog(CRITICAL, "Failed to get size of record: %s", rec_type); - } + if(recordSizeBytes <= 0) throw RunTimeException(CRITICAL, RTE_ERROR, "Failed to get record size for %s", rec_type); /* Get Index Field (e.g. Extent Id) */ - indexField = RecordObject::getDefinedField(rec_type, index_key); - if(indexField.type == RecordObject::INVALID_FIELD) - { - mlog(CRITICAL, "Failed to get field %s from record type: %s", index_key, rec_type); - } + indexField = RecordObject::getDefinedField(rec_type, rec_meta->index_field); + if(indexField.type == RecordObject::INVALID_FIELD) throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to get index field <%s> for %s", rec_meta->index_field, rec_type); /* Get Longitude Field */ - lonField = RecordObject::getDefinedField(rec_type, lon_key); - if(lonField.type == RecordObject::INVALID_FIELD) - { - mlog(CRITICAL, "Failed to get field %s from record type: %s", lon_key, rec_type); - } + lonField = RecordObject::getDefinedField(rec_type, rec_meta->x_field); + if(lonField.type == RecordObject::INVALID_FIELD) throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to get longitude field <%s> for %s", rec_meta->x_field, rec_type); /* Get Latitude Field */ - latField = RecordObject::getDefinedField(rec_type, lat_key); - if(latField.type == RecordObject::INVALID_FIELD) - { - mlog(CRITICAL, "Failed to get field %s from record type: %s", lat_key, rec_type); - } + latField = RecordObject::getDefinedField(rec_type, rec_meta->y_field); + if(latField.type == RecordObject::INVALID_FIELD) throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to get latitude field <%s> for %s", rec_meta->y_field, rec_type); /* Get Time Field */ timeField.type = RecordObject::INVALID_FIELD; - if(time_key) + if(use_time) { - timeField = RecordObject::getDefinedField(rec_type, time_key); - if(timeField.type == RecordObject::INVALID_FIELD) - { - mlog(CRITICAL, "Failed to get field %s from record type: %s", time_key, rec_type); - } + timeField = RecordObject::getDefinedField(rec_type, rec_meta->time_field); + if(timeField.type == RecordObject::INVALID_FIELD) throw RunTimeException(CRITICAL, RTE_ERROR, "Unable to get time field <%s> for %s", rec_meta->time_field, rec_type); } - /* Get Height Field */ - heightField.type = RecordObject::INVALID_FIELD; - if(height_key) - { - heightField = RecordObject::getDefinedField(rec_type, height_key); - if(heightField.type == RecordObject::INVALID_FIELD) - { - mlog(CRITICAL, "Failed to get field %s from record type: %s", height_key, rec_type); - } - } + /* Get Height Field + * code below lets it be invalid if no height field is present */ + heightField = RecordObject::getDefinedField(rec_type, rec_meta->z_field); + + /* Initialize Class Data */ + raster = _raster; + rasterKey = StringLib::duplicate(raster_key); + outQ = new Publisher(outq_name); } /*---------------------------------------------------------------------------- diff --git a/packages/geo/RasterSampler.h b/packages/geo/RasterSampler.h index 9cf9bbf1f..dc400ad9a 100644 --- a/packages/geo/RasterSampler.h +++ b/packages/geo/RasterSampler.h @@ -149,9 +149,7 @@ class RasterSampler: public DispatchObject *--------------------------------------------------------------------*/ RasterSampler (lua_State* L, RasterObject* _raster, const char* raster_key, - const char* outq_name, const char* rec_type, - const char* index_key, const char* lon_key, const char* lat_key, - const char* time_key, const char* height_key); + const char* outq_name, const char* rec_type, bool use_time); ~RasterSampler (void); bool processRecord (RecordObject* record, okey_t key, recVec_t* records) override; diff --git a/plugins/gedi/endpoints/gedi01b.lua b/plugins/gedi/endpoints/gedi01b.lua index 8455fa341..55649d7cf 100644 --- a/plugins/gedi/endpoints/gedi01b.lua +++ b/plugins/gedi/endpoints/gedi01b.lua @@ -14,11 +14,6 @@ local args = { default_asset = "gedil1b", result_q = parms[geo.PARMS] and "result." .. resource .. "." .. rspq or rspq, result_rec = "gedi01brec", - index_field = "footprint.shot_number", - lon_field = "footprint.longitude", - lat_field = "footprint.latitude", - time_field = "footprint.time", - height_field = "footprint.elevation_start" } local rqst_parms = gedi.parms(parms) diff --git a/plugins/gedi/endpoints/gedi01bp.lua b/plugins/gedi/endpoints/gedi01bp.lua index e2443ccdd..d3687382b 100644 --- a/plugins/gedi/endpoints/gedi01bp.lua +++ b/plugins/gedi/endpoints/gedi01bp.lua @@ -9,4 +9,4 @@ local rqst = json.decode(arg[1]) local resources = rqst["resources"] local parms = rqst["parms"] -proxy.proxy(resources, parms, "gedi01b", "gedi01b", "footprint.longitude", "footprint.latitude") +proxy.proxy(resources, parms, "gedi01b", "gedi01b") diff --git a/plugins/gedi/endpoints/gedi02a.lua b/plugins/gedi/endpoints/gedi02a.lua index c00fffd8d..8ab463a7a 100644 --- a/plugins/gedi/endpoints/gedi02a.lua +++ b/plugins/gedi/endpoints/gedi02a.lua @@ -14,11 +14,6 @@ local args = { default_asset = "gedi02a", result_q = parms[geo.PARMS] and "result." .. resource .. "." .. rspq or rspq, result_rec = "gedi02arec", - index_field = "footprint.shot_number", - lon_field = "footprint.longitude", - lat_field = "footprint.latitude", - time_field = "footprint.time", - height_field = "footprint.elevation_lm" } local rqst_parms = gedi.parms(parms) diff --git a/plugins/gedi/endpoints/gedi02ap.lua b/plugins/gedi/endpoints/gedi02ap.lua index 44e952de0..2ac05f591 100644 --- a/plugins/gedi/endpoints/gedi02ap.lua +++ b/plugins/gedi/endpoints/gedi02ap.lua @@ -9,4 +9,4 @@ local rqst = json.decode(arg[1]) local resources = rqst["resources"] local parms = rqst["parms"] -proxy.proxy(resources, parms, "gedi02a", "gedi02a", "footprint.longitude", "footprint.latitude") +proxy.proxy(resources, parms, "gedi02a", "gedi02a") diff --git a/plugins/gedi/endpoints/gedi04a.lua b/plugins/gedi/endpoints/gedi04a.lua index d13d2c015..b0bb52e65 100644 --- a/plugins/gedi/endpoints/gedi04a.lua +++ b/plugins/gedi/endpoints/gedi04a.lua @@ -14,11 +14,6 @@ local args = { default_asset = "gedi04a", result_q = parms[geo.PARMS] and "result." .. resource .. "." .. rspq or rspq, result_rec = "gedi04arec", - index_field = "footprint.shot_number", - lon_field = "footprint.longitude", - lat_field = "footprint.latitude", - time_field = "footprint.time", - height_field = "footprint.elevation" } local rqst_parms = gedi.parms(parms) diff --git a/plugins/gedi/endpoints/gedi04ap.lua b/plugins/gedi/endpoints/gedi04ap.lua index 366262729..3223beb02 100644 --- a/plugins/gedi/endpoints/gedi04ap.lua +++ b/plugins/gedi/endpoints/gedi04ap.lua @@ -9,4 +9,4 @@ local rqst = json.decode(arg[1]) local resources = rqst["resources"] local parms = rqst["parms"] -proxy.proxy(resources, parms, "gedi04a", "gedi04a", "footprint.longitude", "footprint.latitude") +proxy.proxy(resources, parms, "gedi04a", "gedi04a") diff --git a/plugins/gedi/plugin/Gedi01bReader.cpp b/plugins/gedi/plugin/Gedi01bReader.cpp index 862d25fc7..2128013de 100644 --- a/plugins/gedi/plugin/Gedi01bReader.cpp +++ b/plugins/gedi/plugin/Gedi01bReader.cpp @@ -51,7 +51,7 @@ const RecordObject::fieldDef_t Gedi01bReader::fpRecDef[] = { {"time", RecordObject::TIME8, offsetof(g01b_footprint_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, {"latitude", RecordObject::DOUBLE, offsetof(g01b_footprint_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, {"longitude", RecordObject::DOUBLE, offsetof(g01b_footprint_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, - {"elevation_start", RecordObject::DOUBLE, offsetof(g01b_footprint_t, elevation_start), 1, NULL, NATIVE_FLAGS}, + {"elevation_start", RecordObject::DOUBLE, offsetof(g01b_footprint_t, elevation_start), 1, NULL, NATIVE_FLAGS | RecordObject::Z_COORD}, {"elevation_stop", RecordObject::DOUBLE, offsetof(g01b_footprint_t, elevation_stop), 1, NULL, NATIVE_FLAGS}, {"solar_elevation", RecordObject::DOUBLE, offsetof(g01b_footprint_t, solar_elevation), 1, NULL, NATIVE_FLAGS}, {"beam", RecordObject::UINT8, offsetof(g01b_footprint_t, beam), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/gedi/plugin/Gedi02aReader.cpp b/plugins/gedi/plugin/Gedi02aReader.cpp index 0ea25c24a..edca265ea 100644 --- a/plugins/gedi/plugin/Gedi02aReader.cpp +++ b/plugins/gedi/plugin/Gedi02aReader.cpp @@ -51,7 +51,7 @@ const RecordObject::fieldDef_t Gedi02aReader::fpRecDef[] = { {"time", RecordObject::TIME8, offsetof(g02a_footprint_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, {"latitude", RecordObject::DOUBLE, offsetof(g02a_footprint_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, {"longitude", RecordObject::DOUBLE, offsetof(g02a_footprint_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, - {"elevation_lm", RecordObject::FLOAT, offsetof(g02a_footprint_t, elevation_lowestmode), 1, NULL, NATIVE_FLAGS}, + {"elevation_lm", RecordObject::FLOAT, offsetof(g02a_footprint_t, elevation_lowestmode), 1, NULL, NATIVE_FLAGS | RecordObject::Z_COORD}, {"elevation_hr", RecordObject::FLOAT, offsetof(g02a_footprint_t, elevation_highestreturn), 1, NULL, NATIVE_FLAGS}, {"solar_elevation", RecordObject::FLOAT, offsetof(g02a_footprint_t, solar_elevation), 1, NULL, NATIVE_FLAGS}, {"sensitivity", RecordObject::FLOAT, offsetof(g02a_footprint_t, sensitivity), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/gedi/plugin/Gedi04aReader.cpp b/plugins/gedi/plugin/Gedi04aReader.cpp index e5e9fb480..252712283 100644 --- a/plugins/gedi/plugin/Gedi04aReader.cpp +++ b/plugins/gedi/plugin/Gedi04aReader.cpp @@ -52,7 +52,7 @@ const RecordObject::fieldDef_t Gedi04aReader::fpRecDef[] = { {"latitude", RecordObject::DOUBLE, offsetof(g04a_footprint_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, {"longitude", RecordObject::DOUBLE, offsetof(g04a_footprint_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, {"agbd", RecordObject::FLOAT, offsetof(g04a_footprint_t, agbd), 1, NULL, NATIVE_FLAGS}, - {"elevation", RecordObject::FLOAT, offsetof(g04a_footprint_t, elevation), 1, NULL, NATIVE_FLAGS}, + {"elevation", RecordObject::FLOAT, offsetof(g04a_footprint_t, elevation), 1, NULL, NATIVE_FLAGS | RecordObject::Z_COORD}, {"solar_elevation", RecordObject::FLOAT, offsetof(g04a_footprint_t, solar_elevation), 1, NULL, NATIVE_FLAGS}, {"sensitivity", RecordObject::FLOAT, offsetof(g04a_footprint_t, sensitivity), 1, NULL, NATIVE_FLAGS}, {"beam", RecordObject::UINT8, offsetof(g04a_footprint_t, beam), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/icesat2/endpoints/atl03s.lua b/plugins/icesat2/endpoints/atl03s.lua index 9e9156918..533bed319 100644 --- a/plugins/icesat2/endpoints/atl03s.lua +++ b/plugins/icesat2/endpoints/atl03s.lua @@ -14,11 +14,6 @@ local args = { default_asset = "icesat2", result_q = parms[geo.PARMS] and "result." .. resource .. "." .. rspq or rspq, result_rec = "atl03rec", - index_field = "extent_id", - lon_field = "photons.longitude", - lat_field = "photons.latitude", - time_field = "photons.time", - height_field = "photons.height" } local rqst_parms = icesat2.parms(parms) diff --git a/plugins/icesat2/endpoints/atl03sp.lua b/plugins/icesat2/endpoints/atl03sp.lua index 165f2aadd..579022873 100644 --- a/plugins/icesat2/endpoints/atl03sp.lua +++ b/plugins/icesat2/endpoints/atl03sp.lua @@ -9,4 +9,4 @@ local rqst = json.decode(arg[1]) local resources = rqst["resources"] local parms = rqst["parms"] -proxy.proxy(resources, parms, "atl03s", "atl03rec", "photons.longitude", "photons.latitude") +proxy.proxy(resources, parms, "atl03s", "atl03rec") diff --git a/plugins/icesat2/endpoints/atl06.lua b/plugins/icesat2/endpoints/atl06.lua index 61fcde21d..8e82d2ff5 100644 --- a/plugins/icesat2/endpoints/atl06.lua +++ b/plugins/icesat2/endpoints/atl06.lua @@ -15,11 +15,6 @@ local args = { result_q = parms[geo.PARMS] and "result." .. resource .. "." .. rspq or rspq, source_rec = "atl03rec", result_rec = "atl06rec", - index_field = "elevation.extent_id", - lon_field = "elevation.longitude", - lat_field = "elevation.latitude", - time_field = "elevation.time", - height_field = "elevation.h_mean" } local rqst_parms = icesat2.parms(parms) diff --git a/plugins/icesat2/endpoints/atl06p.lua b/plugins/icesat2/endpoints/atl06p.lua index 8599f8aea..43c9ab58a 100644 --- a/plugins/icesat2/endpoints/atl06p.lua +++ b/plugins/icesat2/endpoints/atl06p.lua @@ -9,4 +9,4 @@ local rqst = json.decode(arg[1]) local resources = rqst["resources"] local parms = rqst["parms"] -proxy.proxy(resources, parms, "atl06", "atl06rec", "elevation.longitude", "elevation.latitude") +proxy.proxy(resources, parms, "atl06", "atl06rec") diff --git a/plugins/icesat2/endpoints/atl06s.lua b/plugins/icesat2/endpoints/atl06s.lua index 235e473ec..806319fb2 100644 --- a/plugins/icesat2/endpoints/atl06s.lua +++ b/plugins/icesat2/endpoints/atl06s.lua @@ -14,11 +14,6 @@ local args = { default_asset = "icesat2", result_q = parms[geo.PARMS] and "result." .. resource .. "." .. rspq or rspq, result_rec = "atl06srec", - index_field = "extent_id", - lon_field = "elevation.longitude", - lat_field = "elevation.latitude", - time_field = "elevation.time", - height_field = "elevation.h_li" } local rqst_parms = icesat2.parms(parms) diff --git a/plugins/icesat2/endpoints/atl06sp.lua b/plugins/icesat2/endpoints/atl06sp.lua index 682539c19..3b2ac45c9 100644 --- a/plugins/icesat2/endpoints/atl06sp.lua +++ b/plugins/icesat2/endpoints/atl06sp.lua @@ -9,4 +9,4 @@ local rqst = json.decode(arg[1]) local resources = rqst["resources"] local parms = rqst["parms"] -proxy.proxy(resources, parms, "atl06s", "atl06srec", "elevation.longitude", "elevation.latitude") +proxy.proxy(resources, parms, "atl06s", "atl06srec") diff --git a/plugins/icesat2/endpoints/atl08.lua b/plugins/icesat2/endpoints/atl08.lua index 74fc938e8..7d5c1d47b 100644 --- a/plugins/icesat2/endpoints/atl08.lua +++ b/plugins/icesat2/endpoints/atl08.lua @@ -15,11 +15,6 @@ local args = { result_q = parms[geo.PARMS] and "result." .. resource .. "." .. rspq or rspq, source_rec = "atl03rec", result_rec = "atl08rec", - index_field = "vegetation.extent_id", - lon_field = "vegetation.longitude", - lat_field = "vegetation.latitude", - time_field = "vegetation.time", - height_field = "vegetation.h_te_median" } local rqst_parms = icesat2.parms(parms) diff --git a/plugins/icesat2/endpoints/atl08p.lua b/plugins/icesat2/endpoints/atl08p.lua index 44e5a9f36..b4ccb1bb7 100644 --- a/plugins/icesat2/endpoints/atl08p.lua +++ b/plugins/icesat2/endpoints/atl08p.lua @@ -9,4 +9,4 @@ local rqst = json.decode(arg[1]) local resources = rqst["resources"] local parms = rqst["parms"] -proxy.proxy(resources, parms, "atl08", "atl08rec", "vegetation.longitude", "vegetation.latitude") +proxy.proxy(resources, parms, "atl08", "atl08rec") diff --git a/plugins/icesat2/plugin/Atl03Reader.cpp b/plugins/icesat2/plugin/Atl03Reader.cpp index 488e4a244..dd7626bc9 100644 --- a/plugins/icesat2/plugin/Atl03Reader.cpp +++ b/plugins/icesat2/plugin/Atl03Reader.cpp @@ -52,7 +52,7 @@ const RecordObject::fieldDef_t Atl03Reader::phRecDef[] = { {"longitude", RecordObject::DOUBLE, offsetof(photon_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, {"x_atc", RecordObject::FLOAT, offsetof(photon_t, x_atc), 1, NULL, NATIVE_FLAGS}, {"y_atc", RecordObject::FLOAT, offsetof(photon_t, y_atc), 1, NULL, NATIVE_FLAGS}, - {"height", RecordObject::FLOAT, offsetof(photon_t, height), 1, NULL, NATIVE_FLAGS}, + {"height", RecordObject::FLOAT, offsetof(photon_t, height), 1, NULL, NATIVE_FLAGS | RecordObject::Z_COORD}, {"relief", RecordObject::FLOAT, offsetof(photon_t, relief), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"landcover", RecordObject::UINT8, offsetof(photon_t, landcover), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"snowcover", RecordObject::UINT8, offsetof(photon_t, snowcover), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, diff --git a/plugins/icesat2/plugin/Atl06Dispatch.cpp b/plugins/icesat2/plugin/Atl06Dispatch.cpp index ea9f40c52..b992fd613 100644 --- a/plugins/icesat2/plugin/Atl06Dispatch.cpp +++ b/plugins/icesat2/plugin/Atl06Dispatch.cpp @@ -97,7 +97,7 @@ const RecordObject::fieldDef_t Atl06Dispatch::elRecDef[] = { {"time", RecordObject::TIME8, offsetof(elevation_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, {"latitude", RecordObject::DOUBLE, offsetof(elevation_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, {"longitude", RecordObject::DOUBLE, offsetof(elevation_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, - {"h_mean", RecordObject::DOUBLE, offsetof(elevation_t, h_mean), 1, NULL, NATIVE_FLAGS}, + {"h_mean", RecordObject::DOUBLE, offsetof(elevation_t, h_mean), 1, NULL, NATIVE_FLAGS | RecordObject::Z_COORD}, {"dh_fit_dx", RecordObject::FLOAT, offsetof(elevation_t, dh_fit_dx), 1, NULL, NATIVE_FLAGS | RecordObject::AUX}, {"x_atc", RecordObject::FLOAT, offsetof(elevation_t, x_atc), 1, NULL, NATIVE_FLAGS}, {"y_atc", RecordObject::FLOAT, offsetof(elevation_t, y_atc), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/icesat2/plugin/Atl06Reader.cpp b/plugins/icesat2/plugin/Atl06Reader.cpp index 614f2a357..e85d3635a 100644 --- a/plugins/icesat2/plugin/Atl06Reader.cpp +++ b/plugins/icesat2/plugin/Atl06Reader.cpp @@ -58,7 +58,7 @@ const RecordObject::fieldDef_t Atl06Reader::elRecDef[] = { {"gt", RecordObject::UINT8, offsetof(elevation_t, gt), 1, NULL, NATIVE_FLAGS}, // land_ice_segments {"time", RecordObject::TIME8, offsetof(elevation_t, time_ns), 1, NULL, NATIVE_FLAGS | RecordObject::TIME}, - {"h_li", RecordObject::FLOAT, offsetof(elevation_t, h_li), 1, NULL, NATIVE_FLAGS}, + {"h_li", RecordObject::FLOAT, offsetof(elevation_t, h_li), 1, NULL, NATIVE_FLAGS | RecordObject::Z_COORD}, {"h_li_sigma", RecordObject::FLOAT, offsetof(elevation_t, h_li_sigma), 1, NULL, NATIVE_FLAGS}, {"latitude", RecordObject::DOUBLE, offsetof(elevation_t, latitude), 1, NULL, NATIVE_FLAGS | RecordObject::Y_COORD}, {"longitude", RecordObject::DOUBLE, offsetof(elevation_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, diff --git a/plugins/icesat2/plugin/Atl08Dispatch.cpp b/plugins/icesat2/plugin/Atl08Dispatch.cpp index 2489e42d5..fcaa488a3 100644 --- a/plugins/icesat2/plugin/Atl08Dispatch.cpp +++ b/plugins/icesat2/plugin/Atl08Dispatch.cpp @@ -67,7 +67,7 @@ const RecordObject::fieldDef_t Atl08Dispatch::vegRecDef[] = { {"longitude", RecordObject::DOUBLE, offsetof(vegetation_t, longitude), 1, NULL, NATIVE_FLAGS | RecordObject::X_COORD}, {"x_atc", RecordObject::DOUBLE, offsetof(vegetation_t, x_atc), 1, NULL, NATIVE_FLAGS}, {"solar_elevation", RecordObject::FLOAT, offsetof(vegetation_t, solar_elevation), 1, NULL, NATIVE_FLAGS}, - {"h_te_median", RecordObject::FLOAT, offsetof(vegetation_t, h_te_median), 1, NULL, NATIVE_FLAGS}, + {"h_te_median", RecordObject::FLOAT, offsetof(vegetation_t, h_te_median), 1, NULL, NATIVE_FLAGS | RecordObject::Z_COORD}, {"h_max_canopy", RecordObject::FLOAT, offsetof(vegetation_t, h_max_canopy), 1, NULL, NATIVE_FLAGS}, {"h_min_canopy", RecordObject::FLOAT, offsetof(vegetation_t, h_min_canopy), 1, NULL, NATIVE_FLAGS}, {"h_mean_canopy", RecordObject::FLOAT, offsetof(vegetation_t, h_mean_canopy), 1, NULL, NATIVE_FLAGS}, diff --git a/plugins/swot/endpoints/swotl2.lua b/plugins/swot/endpoints/swotl2.lua index 9377bd9a9..b1328ecdc 100644 --- a/plugins/swot/endpoints/swotl2.lua +++ b/plugins/swot/endpoints/swotl2.lua @@ -14,9 +14,6 @@ local args = { default_asset = "swot-sim-ecco-llc4320", result_q = parms[geo.PARMS] and "result." .. resource .. "." .. rspq or rspq, result_rec = "swotl2geo", - index_field = "scan.scan_id", - lon_field = "scan.longitude", - lat_field = "scan.latitude" } local rqst_parms = swot.parms(parms) diff --git a/plugins/swot/endpoints/swotl2p.lua b/plugins/swot/endpoints/swotl2p.lua index e58aa265f..132bd8772 100644 --- a/plugins/swot/endpoints/swotl2p.lua +++ b/plugins/swot/endpoints/swotl2p.lua @@ -9,4 +9,4 @@ local rqst = json.decode(arg[1]) local resources = rqst["resources"] local parms = rqst["parms"] -proxy.proxy(resources, parms, "swotl2", "swotl2var", nil, nil) +proxy.proxy(resources, parms, "swotl2", "swotl2var") diff --git a/scripts/extensions/georesource.lua b/scripts/extensions/georesource.lua index b5bd1e3ed..224998846 100644 --- a/scripts/extensions/georesource.lua +++ b/scripts/extensions/georesource.lua @@ -42,10 +42,9 @@ local function initialize(resource, parms, algo, args) rsps_bridge = core.bridge(args.result_q, rspq) sampler_disp = core.dispatcher(args.result_q, 1) -- 1 thread required because GeoRaster is not thread safe for key,settings in pairs(parms[geo.PARMS]) do - local time_field = settings["use_poi_time"] and (args.time_field or "time") or nil local robj = geo.raster(geo.parms(settings):keyspace(args.shard)) if robj then - local sampler = geo.sampler(robj, key, rspq, args.result_rec, args.index_field, args.lon_field, args.lat_field, time_field, args.height_field) + local sampler = geo.sampler(robj, key, rspq, args.result_rec, settings["use_poi_time"]) if sampler then sampler_disp:attach(sampler, args.result_rec) else diff --git a/scripts/extensions/proxy.lua b/scripts/extensions/proxy.lua index 7dc14cfb4..b5c3de47f 100644 --- a/scripts/extensions/proxy.lua +++ b/scripts/extensions/proxy.lua @@ -16,7 +16,7 @@ local json = require("json") -local function proxy(resources, parms, endpoint, rec, lon, lat) +local function proxy(resources, parms, endpoint, rec) -- Create User Status -- local userlog = msg.publish(rspq) @@ -34,7 +34,7 @@ local function proxy(resources, parms, endpoint, rec, lon, lat) local output_parms = arrow.parms(parms[arrow.PARMS]) -- Parquet Writer -- if output_parms:isparquet() then - parquet_builder = arrow.parquet(output_parms, rspq, rspq .. "-parquet", rec, rqstid, lon, lat, "time") + parquet_builder = arrow.parquet(output_parms, rspq, rspq .. "-parquet", rec, rqstid) if parquet_builder then rsps_from_nodes = rspq .. "-parquet" terminate_proxy_stream = true From 44e95e8d83a97e5824c13067c78f36d904b63eb9 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 29 Feb 2024 22:21:59 +0000 Subject: [PATCH 27/43] fixed trimming of pandas index key --- clients/python/tests/test_parquet.py | 1 + docs/rtd/source/archive/SlideRuleWebClient.md | 3 +++ docs/rtd/source/user_guide/ICESat-2.rst | 3 +++ packages/arrow/ArrowImpl.cpp | 22 ++++++++++++++----- packages/arrow/ArrowImpl.h | 5 +---- packages/arrow/ParquetBuilder.cpp | 19 ++++++++-------- packages/arrow/ParquetBuilder.h | 4 ++-- 7 files changed, 35 insertions(+), 22 deletions(-) diff --git a/clients/python/tests/test_parquet.py b/clients/python/tests/test_parquet.py index d2021e614..a65e33367 100644 --- a/clients/python/tests/test_parquet.py +++ b/clients/python/tests/test_parquet.py @@ -66,6 +66,7 @@ def test_atl03(self, init): "maxi": 1, "output": { "path": "testfile2.parquet", "format": "parquet", "open_on_complete": True } } gdf = icesat2.atl03sp(parms, resources=[resource]) + print(gdf.keys()) os.remove("testfile2.parquet") assert init assert len(gdf) == 190491 diff --git a/docs/rtd/source/archive/SlideRuleWebClient.md b/docs/rtd/source/archive/SlideRuleWebClient.md index dd576d865..176741af2 100644 --- a/docs/rtd/source/archive/SlideRuleWebClient.md +++ b/docs/rtd/source/archive/SlideRuleWebClient.md @@ -290,3 +290,6 @@ The following raster datasets shall be supported for sampling: #### SRWC-5.2: Request Parameters All request parameters supported by SlideRule for a given request shall be supported by the web client. + + +## Appendix A. Parameter Components diff --git a/docs/rtd/source/user_guide/ICESat-2.rst b/docs/rtd/source/user_guide/ICESat-2.rst index e8f357230..3183cd917 100644 --- a/docs/rtd/source/user_guide/ICESat-2.rst +++ b/docs/rtd/source/user_guide/ICESat-2.rst @@ -23,6 +23,7 @@ The photon-input parameters allow the user to select an area, a time range, or a * ``"poly"``: polygon defining region of interest (see `polygons `_) * ``"raster"``: geojson describing region of interest which enables rasterized subsetting on servers (see `geojson `_) * ``"track"``: reference pair track number (1, 2, 3, or 0 to include for all three; defaults to 0) +* ``"beam"``: list of beam identifiers (gt1l, gt1r, gt2l, gt2r, gt3l, gt3r; defaults to all) * ``"rgt"``: reference ground track (defaults to all if not specified) * ``"cycle"``: counter of 91-day repeat cycles completed by the mission (defaults to all if not specified) * ``"region"``: geographic region for corresponding standard product (defaults to all if not specified) @@ -341,6 +342,7 @@ The elevation GeoDataFrame has the following columns: - ``"pflags"``: processing flags (0x1 - spread too short; 0x2 - too few photons; 0x4 - max iterations reached) - ``"rgt"``: reference ground track - ``"cycle"``: cycle +- ``"region"``: region of source granule - ``"spot"``: laser spot 1 to 6 - ``"gt"``: ground track (10: GT1L, 20: GT1R, 30: GT2L, 40: GT2R, 50: GT3L, 60: GT3R) - ``"x_atc"``: along track distance from the equator in meters @@ -365,6 +367,7 @@ The vegetation GeoDataFrame has the following columns: - ``"segment_id"``: segment ID of first ATL03 segment in result - ``"rgt"``: reference ground track - ``"cycle"``: cycle +- ``"region"``: region of source granule - ``"spot"``: laser spot 1 to 6 - ``"gt"``: ground track (10: GT1L, 20: GT1R, 30: GT2L, 40: GT2R, 50: GT3L, 60: GT3R) - ``"ph_count"``: total number of photons used by PhoREAL algorithm for this extent diff --git a/packages/arrow/ArrowImpl.cpp b/packages/arrow/ArrowImpl.cpp index 2f67a902a..7db3e3ac8 100644 --- a/packages/arrow/ArrowImpl.cpp +++ b/packages/arrow/ArrowImpl.cpp @@ -73,7 +73,7 @@ ArrowImpl::~ArrowImpl (void) /*---------------------------------------------------------------------------- * processRecordBatch *----------------------------------------------------------------------------*/ -bool ArrowImpl::processRecordBatch (Ordering& record_batch, int num_rows, int batch_row_size_bits, ParquetBuilder::geo_data_t& geo_data, bool file_finished) +bool ArrowImpl::processRecordBatch (Ordering& record_batch, int num_rows, int batch_row_size_bits, bool file_finished) { bool status = false; @@ -99,11 +99,11 @@ bool ArrowImpl::processRecordBatch (Ordering& record_ba } /* Add Geometry Column (if GeoParquet) */ - if(geo_data.as_geo) + if(parquetBuilder->getAsGeo()) { uint32_t geo_trace_id = start_trace(INFO, trace_id, "geo_column", "%s", "{}"); shared_ptr column; - processGeometry(geo_data.x_field, geo_data.y_field, &column, record_batch, num_rows, batch_row_size_bits); + processGeometry(parquetBuilder->getXField(), parquetBuilder->getYField(), &column, record_batch, num_rows, batch_row_size_bits); columns.push_back(column); stop_trace(INFO, geo_trace_id); } @@ -479,13 +479,23 @@ void ArrowImpl::appendPandasMetaData (const std::shared_ptrgetIndexKey(); - FString indexstr("\"%s\"", index_key ? index_key : ""); + const char* time_key = parquetBuilder->getTimeKey(); + if(!time_key) time_key = ""; // replace null with empty string + string time_str(time_key); + int max_loops = 10; // upper bound the nesting + for(int i = 0; i < max_loops; i++) + { + /* Remove Parent Fields */ + size_t pos = time_str.find("."); + if(pos != string::npos) time_str.replace(0, pos+1, ""); + else break; + } + FString formatted_time("\"%s\"", time_str.c_str()); /* Fill In Pandas Meta Data String */ pandasstr = std::regex_replace(pandasstr, std::regex(" "), ""); pandasstr = std::regex_replace(pandasstr, std::regex("\n"), " "); - pandasstr = std::regex_replace(pandasstr, std::regex("_INDEX_"), index_key ? indexstr.c_str() : ""); + pandasstr = std::regex_replace(pandasstr, std::regex("_INDEX_"), time_key[0] ? formatted_time.c_str() : ""); pandasstr = std::regex_replace(pandasstr, std::regex("_COLUMNS_"), columns.c_str()); /* Append Meta String */ diff --git a/packages/arrow/ArrowImpl.h b/packages/arrow/ArrowImpl.h index a387056ce..f82b058f1 100644 --- a/packages/arrow/ArrowImpl.h +++ b/packages/arrow/ArrowImpl.h @@ -72,10 +72,7 @@ class ArrowImpl ~ArrowImpl (void); bool processRecordBatch (Ordering& record_batch, - int num_rows, - int batch_row_size_bits, - ParquetBuilder::geo_data_t& geo_data, - bool file_finished=false); + int num_rows, int batch_row_size_bits, bool file_finished=false); private: diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index 98cfc6ff0..eab841e8f 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -137,11 +137,11 @@ const char* ParquetBuilder::getRecType (void) } /*---------------------------------------------------------------------------- - * getIndexKey + * getTimeKey *----------------------------------------------------------------------------*/ -const char* ParquetBuilder::getIndexKey (void) +const char* ParquetBuilder::getTimeKey (void) { - return indexKey; + return timeKey; } /*---------------------------------------------------------------------------- @@ -256,10 +256,9 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, /* Set Record Type */ recType = StringLib::duplicate(rec_type); - /* Save Index Key */ - indexKey = StringLib::duplicate(rec_meta->index_field); - printf("INDEX KEY: %s\n", indexKey); - + /* Save Time Key */ + timeKey = StringLib::duplicate(rec_meta->time_field); + /* Get Row Size */ RecordObject::field_t batch_rec_field = RecordObject::getDefinedField(recType, rec_meta->batch_field); if(batch_rec_field.type == RecordObject::INVALID_FIELD) batchRowSizeBytes = 0; @@ -295,7 +294,7 @@ ParquetBuilder::~ParquetBuilder(void) delete [] fileName; delete [] outputPath; delete [] recType; - delete [] indexKey; + delete [] timeKey; delete outQ; delete inQ; delete impl; @@ -359,7 +358,7 @@ void* ParquetBuilder::builderThread(void* parm) row_cnt += num_rows; if(row_cnt >= builder->maxRowsInGroup) { - bool status = builder->impl->processRecordBatch(builder->recordBatch, row_cnt, builder->batchRowSizeBytes * 8, builder->geoData); + bool status = builder->impl->processRecordBatch(builder->recordBatch, row_cnt, builder->batchRowSizeBytes * 8); if(!status) { alert(RTE_ERROR, INFO, builder->outQ, NULL, "Failed to process record batch for %s", builder->outputPath); @@ -386,7 +385,7 @@ void* ParquetBuilder::builderThread(void* parm) } /* Process Remaining Records */ - bool status = builder->impl->processRecordBatch(builder->recordBatch, row_cnt, builder->batchRowSizeBytes * 8, builder->geoData, true); + bool status = builder->impl->processRecordBatch(builder->recordBatch, row_cnt, builder->batchRowSizeBytes * 8, true); if(!status) alert(RTE_ERROR, INFO, builder->outQ, NULL, "Failed to process last record batch for %s", builder->outputPath); builder->clearBatch(trace_id); diff --git a/packages/arrow/ParquetBuilder.h b/packages/arrow/ParquetBuilder.h index b81a80905..bbf28a3af 100644 --- a/packages/arrow/ParquetBuilder.h +++ b/packages/arrow/ParquetBuilder.h @@ -132,7 +132,7 @@ class ParquetBuilder: public LuaObject const char* getFileName (void); const char* getRecType (void); - const char* getIndexKey (void); + const char* getTimeKey (void); bool getAsGeo (void); RecordObject::field_t& getXField (void); RecordObject::field_t& getYField (void); @@ -148,7 +148,7 @@ class ParquetBuilder: public LuaObject bool active; Subscriber* inQ; const char* recType; - const char* indexKey; + const char* timeKey; Ordering recordBatch; Publisher* outQ; int rowSizeBytes; From 8ea2ac567e307493825527975f9a12f7492cfff6 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 29 Feb 2024 22:26:01 +0000 Subject: [PATCH 28/43] fixed parquet pytest - number of keys in atl03 should account for lat and lon not being there --- clients/python/tests/test_parquet.py | 1 - 1 file changed, 1 deletion(-) diff --git a/clients/python/tests/test_parquet.py b/clients/python/tests/test_parquet.py index a65e33367..d2021e614 100644 --- a/clients/python/tests/test_parquet.py +++ b/clients/python/tests/test_parquet.py @@ -66,7 +66,6 @@ def test_atl03(self, init): "maxi": 1, "output": { "path": "testfile2.parquet", "format": "parquet", "open_on_complete": True } } gdf = icesat2.atl03sp(parms, resources=[resource]) - print(gdf.keys()) os.remove("testfile2.parquet") assert init assert len(gdf) == 190491 From e9a9425a3362cc3b29442c44656d5528b105c181 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 29 Feb 2024 22:26:51 +0000 Subject: [PATCH 29/43] fixed parquet pytest - number of keys in atl03 should account for lat and lon not being there --- clients/python/tests/test_parquet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/python/tests/test_parquet.py b/clients/python/tests/test_parquet.py index d2021e614..6c468e543 100644 --- a/clients/python/tests/test_parquet.py +++ b/clients/python/tests/test_parquet.py @@ -69,7 +69,7 @@ def test_atl03(self, init): os.remove("testfile2.parquet") assert init assert len(gdf) == 190491 - assert len(gdf.keys()) == 24 + assert len(gdf.keys()) == 22 assert gdf["rgt"][0] == 1160 assert gdf["cycle"][0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 From 25b410d9818cca957a18d665c74b2a24e8aefbb3 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Thu, 29 Feb 2024 23:14:04 +0000 Subject: [PATCH 30/43] added beams to icesat2 parms --- docs/rtd/source/archive/SlideRuleWebClient.md | 95 +++++++++++++++++++ plugins/icesat2/plugin/Icesat2Parms.cpp | 6 ++ plugins/icesat2/plugin/Icesat2Parms.h | 2 +- 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/docs/rtd/source/archive/SlideRuleWebClient.md b/docs/rtd/source/archive/SlideRuleWebClient.md index 176741af2..c7facd987 100644 --- a/docs/rtd/source/archive/SlideRuleWebClient.md +++ b/docs/rtd/source/archive/SlideRuleWebClient.md @@ -293,3 +293,98 @@ All request parameters supported by SlideRule for a given request shall be suppo ## Appendix A. Parameter Components + +* __Mission__: dropdown or select button + - ICESat-2 + - GEDI + +* __API__: dropdown + - atl03s + - atl06 + - atl06s + - atl08 + - atl24s + +* __General__: accordian header + - _Polygon_: label + - _Draw On Map_: radio button + - _Upload_: radio button + - _File Upload_: file upload button + - _Rasterize Polygon_: checkbox + - _Cell Size_: input number (degrees) + - _Ignore Polygon for CMR_: checkbox + - _Projection_: label + - *auto*: radio button + - *plate_carree*: radio button + - *north_polar*: radio button + - *south_polar*: radio button + - _Timeout_: input number (seconds) + - _rqst-timeout_: input number (seconds) + - _node-timeout_: input number (seconds) + - _read-timeout_: input number (seconds) + +* __Granule Selection__: accordian header (ICESat-2) + - _Track_: label + - _1_: checkbox + - _2_: checkbox + - _3_: checkbox + - _all_: checkbox / toggle others + - _Beam_: label + - _gt1l_: checkbox + - _gt1r_: checkbox + - _gt2l_: checkbox + - _gt2r_: checkbox + - _gt3l_: checkbox + - _gt3r_: checkbox + - _all_: checbox / toggle others + - _RGT_: input number + - _Cycle_: input number + - _Region_: input number + - _T0_: calendar + - _T1_: calendar + +* __Photon Selection__: accordian header (ICESat-2) + - _ATL03 Confidence_: input switch + - _Surface Reference Type_: label + - *land*: radio button + - *ocean*: radio button + - *sea ice*: radio button + - *land_ice*: radio button + - *inland_water*: radio button + - _Signal Confidence_: label + - *tep*: radio button + - *not_considered*: radio button + - *background*: radio button + - *within_10m*: radio button + - *low*: radio button + - *medium*: radio button + - *high*: radio button + - _ATL08 Classification_: input switch + - _Land Type_: label + - *noise*: checkbox + - *ground*: checkbox + - *canopy*: checkbox + - *top_of_canopy*: checkbox + - *unclassified*: checkbox + - _ATL03 YAPC_: input switch + - _Score_: input number + - _SR YAPC_: input switch + - _Score_: input number + - _Knn_: input number + - _Window Height_: input number + - _Window Width_: input number + - _Version_: label + - *version 1*: radio button + - *version 2*: radio button + - *version 3*: radio button + + + "score": the minimum yapc classification score of a photon to be used in the processing request + +"knn": the number of nearest neighbors to use, or specify 0 to allow automatic selection of the number of neighbors (recommended) + +"win_h": the window height used to filter the nearest neighbors + +"win_x": the window width used to filter the nearest neighbors + +"version": the version of the YAPC algorithm to use diff --git a/plugins/icesat2/plugin/Icesat2Parms.cpp b/plugins/icesat2/plugin/Icesat2Parms.cpp index bc7731466..9410fdf0d 100644 --- a/plugins/icesat2/plugin/Icesat2Parms.cpp +++ b/plugins/icesat2/plugin/Icesat2Parms.cpp @@ -54,6 +54,7 @@ const char* Icesat2Parms::YAPC_VERSION = "version"; const char* Icesat2Parms::ATL08_CLASS = "atl08_class"; const char* Icesat2Parms::QUALITY = "quality_ph"; const char* Icesat2Parms::TRACK = "track"; +const char* Icesat2Parms::BEAMS = "beams"; const char* Icesat2Parms::STAGES = "stages"; const char* Icesat2Parms::ALONG_TRACK_SPREAD = "ats"; const char* Icesat2Parms::MIN_PHOTON_COUNT = "cnt"; @@ -348,6 +349,11 @@ Icesat2Parms::Icesat2Parms(lua_State* L, int index): if(provided) mlog(DEBUG, "Setting %s to %d", Icesat2Parms::TRACK, track); lua_pop(L, 1); + /* Beams */ + lua_getfield(L, index, Icesat2Parms::BEAMS); + get_lua_beams(L, -1, &provided); + lua_pop(L, 1); + /* Maximum Iterations */ lua_getfield(L, index, Icesat2Parms::MAX_ITERATIONS); max_iterations = LuaObject::getLuaInteger(L, -1, true, max_iterations, &provided); diff --git a/plugins/icesat2/plugin/Icesat2Parms.h b/plugins/icesat2/plugin/Icesat2Parms.h index 95f8db3e0..f5b0137df 100644 --- a/plugins/icesat2/plugin/Icesat2Parms.h +++ b/plugins/icesat2/plugin/Icesat2Parms.h @@ -69,8 +69,8 @@ class Icesat2Parms: public NetsvcParms static const char* ATL08_CLASS; static const char* QUALITY; static const char* TRACK; + static const char* BEAMS; static const char* STAGES; - static const char* COMPACT; static const char* ALONG_TRACK_SPREAD; static const char* MIN_PHOTON_COUNT; static const char* EXTENT_LENGTH; From 8144e5e6381dd285af0d9d3677a524565a95c329 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Fri, 1 Mar 2024 13:33:53 +0000 Subject: [PATCH 31/43] added component list appendix to web gui doc --- docs/rtd/source/archive/SlideRuleWebClient.md | 109 +++++++++++++++--- 1 file changed, 96 insertions(+), 13 deletions(-) diff --git a/docs/rtd/source/archive/SlideRuleWebClient.md b/docs/rtd/source/archive/SlideRuleWebClient.md index c7facd987..305242f50 100644 --- a/docs/rtd/source/archive/SlideRuleWebClient.md +++ b/docs/rtd/source/archive/SlideRuleWebClient.md @@ -298,13 +298,18 @@ All request parameters supported by SlideRule for a given request shall be suppo - ICESat-2 - GEDI -* __API__: dropdown +* __ICESat-2 APIs__: dropdown [ICESat-2] - atl03s - atl06 - atl06s - atl08 - atl24s +* __GEDI APIs__: dropdown [GEDI] + - gedi01b + - gedi02a + - gedi04a + * __General__: accordian header - _Polygon_: label - _Draw On Map_: radio button @@ -323,7 +328,7 @@ All request parameters supported by SlideRule for a given request shall be suppo - _node-timeout_: input number (seconds) - _read-timeout_: input number (seconds) -* __Granule Selection__: accordian header (ICESat-2) +* __Granule Selection__: accordian header [ICESat-2] - _Track_: label - _1_: checkbox - _2_: checkbox @@ -343,7 +348,7 @@ All request parameters supported by SlideRule for a given request shall be suppo - _T0_: calendar - _T1_: calendar -* __Photon Selection__: accordian header (ICESat-2) +* __Photon Selection__: accordian header [ICESat-2] - _ATL03 Confidence_: input switch - _Surface Reference Type_: label - *land*: radio button @@ -378,13 +383,91 @@ All request parameters supported by SlideRule for a given request shall be suppo - *version 2*: radio button - *version 3*: radio button - - "score": the minimum yapc classification score of a photon to be used in the processing request - -"knn": the number of nearest neighbors to use, or specify 0 to allow automatic selection of the number of neighbors (recommended) - -"win_h": the window height used to filter the nearest neighbors - -"win_x": the window width used to filter the nearest neighbors - -"version": the version of the YAPC algorithm to use +* __Extents (Variable-Length Segmentation)__: accordian header [ICESat-2] + - _Length_: input number (meters) + - _Step Size_: input number (meters) + - _Distance in Segments_: checkbox (changes above inputs to segments instead of meters) + - _Pass Invalid_: checkbox + - _Along Track Spread_: input number [greyed out when pass invalid selected] + - _Minimum Photon Count_: input number [greyed out when pass invalid selected] + +* __Surface Elevation Algorithm__: accordian header [atl06] + - _Maximum Iterations_: input number + - _Minimum Window Height_: input number (meters) + - _Maximum Robust Dispersion_: input number (meters) + +* __Vegetation Density Algorithm__: accordian header [atl08] + - _Bin Size_: input number (meters) + - _Geolocation_: label + - _mean_: radio button + - _median_: radio button + - _center_: radio button + - _Use Absoulte Heights_: checkbox + - _Send Waveforms_: checkbox + - _Use ABoVE Classifier_: checkbox + +* __Ancillary Fields__: accordian header [ICESat-2] + - _ATL03 Geospatial Fields_: multiselect [atl03, atl06] + - _ATL03 Photon Fields_: multiselect [atl03, atl06] + - _ATL06 Ice Segment Fields_: multiselect [atl06s] + - _ATL08 Land Segment Fields_: multiselect [atl08] + - _interpolate_: checkbox (next to each field selection) + +* __GEDI Footprint Selection__: accordian header [GEDI] + - _Beam_: multiselect + - 0 + - 1 + - 2 + - 3 + - 5 + - 6 + - 8 + - 11 + - all (toggles selection of others) + - _Degrade Flag_: checkbox + - _L2 Quality Flag_: checkbox + - _L4 Quality Flag_: checkbox [gedi04a] + - _Surface Flag_: checkbox + +* __Raster Sampling__: accordian header + - _Rasters to Sample_: data table (updated with each added entry) + - _Add Entry_: button + - *key*: input text + - *asset*: dropdown + - *algorithm*: dropdown + - NearestNeighbour + - Bilinear + - Cubic + - CubicSpline + - Lanczos + - Average + - Mode + - Gauss + - *radius*: input number (meters) + - *zonal stats*: checkbox + - *with flags*: checkbox + - *t0*: calendar + - *t1*: calendar + - *substring*: input text + - *closest time*: checkbox + - *catalog*: input group + - *user edit*: text area + - *upload*: file upload button (populates text area on upload) + - *bands*: multiselect (selection based on asset field) + +* __Output__: accordian header + - _Enabled_: input switch (shows everything else below when enabled) + - _Staged_: checkbox (greys out path, region, and credentials when selected) + - _Format_: label + - _geoparquet_: radio button + - _parquet_: radio button + - _csv_: radio button + - _Location_: label + - _local_: radio button (greys out region and credentials when selected) + - _s3_: radio button + - _Path_: input text + - _Region_: drowdown + - _Credentials_: file upload button + - *aws_access_key_id*: password + - *aws_secret_access_key*: password + - *aws_session_token*: password From 7b40ac174f23ff2f96fc7f8119b75e7506108ea5 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Fri, 1 Mar 2024 16:22:24 +0000 Subject: [PATCH 32/43] updates to web client components --- docs/rtd/source/archive/SlideRuleWebClient.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/rtd/source/archive/SlideRuleWebClient.md b/docs/rtd/source/archive/SlideRuleWebClient.md index 305242f50..62ff074fd 100644 --- a/docs/rtd/source/archive/SlideRuleWebClient.md +++ b/docs/rtd/source/archive/SlideRuleWebClient.md @@ -349,7 +349,7 @@ All request parameters supported by SlideRule for a given request shall be suppo - _T1_: calendar * __Photon Selection__: accordian header [ICESat-2] - - _ATL03 Confidence_: input switch + - _ATL03 Confidence_: input switch (enables inputs below) - _Surface Reference Type_: label - *land*: radio button - *ocean*: radio button @@ -364,16 +364,16 @@ All request parameters supported by SlideRule for a given request shall be suppo - *low*: radio button - *medium*: radio button - *high*: radio button - - _ATL08 Classification_: input switch + - _ATL08 Classification_: input switch (enables inputs below) - _Land Type_: label - *noise*: checkbox - *ground*: checkbox - *canopy*: checkbox - *top_of_canopy*: checkbox - *unclassified*: checkbox - - _ATL03 YAPC_: input switch + - _ATL03 YAPC_: input switch (enables inputs below) - _Score_: input number - - _SR YAPC_: input switch + - _SR YAPC_: input switch (enables inputs below) - _Score_: input number - _Knn_: input number - _Window Height_: input number From 1f348c8c82ad45290c8ace4cc7c6cbd7117716f3 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Fri, 1 Mar 2024 22:43:39 +0000 Subject: [PATCH 33/43] beginning to add ancillary field support to parquet builder --- packages/arrow/ArrowImpl.cpp | 288 ++++++++---------- packages/arrow/ArrowImpl.h | 18 +- packages/arrow/ParquetBuilder.cpp | 125 +++++--- packages/arrow/ParquetBuilder.h | 53 +++- .../core}/AncillaryFields.cpp | 1 - .../core}/AncillaryFields.h | 8 - packages/core/CMakeLists.txt | 2 + packages/core/core.cpp | 1 + packages/core/core.h | 1 + plugins/icesat2/CMakeLists.txt | 2 - plugins/icesat2/plugin/Atl03Reader.cpp | 8 +- plugins/icesat2/plugin/Atl03Reader.h | 2 +- plugins/icesat2/plugin/Atl06Reader.cpp | 2 +- plugins/icesat2/plugin/Icesat2Parms.h | 8 + plugins/icesat2/plugin/icesat2.cpp | 1 - plugins/icesat2/plugin/icesat2.h | 1 - 16 files changed, 284 insertions(+), 237 deletions(-) rename {plugins/icesat2/plugin => packages/core}/AncillaryFields.cpp (99%) rename {plugins/icesat2/plugin => packages/core}/AncillaryFields.h (96%) diff --git a/packages/arrow/ArrowImpl.cpp b/packages/arrow/ArrowImpl.cpp index 7db3e3ac8..d4143377a 100644 --- a/packages/arrow/ArrowImpl.cpp +++ b/packages/arrow/ArrowImpl.cpp @@ -73,7 +73,7 @@ ArrowImpl::~ArrowImpl (void) /*---------------------------------------------------------------------------- * processRecordBatch *----------------------------------------------------------------------------*/ -bool ArrowImpl::processRecordBatch (Ordering& record_batch, int num_rows, int batch_row_size_bits, bool file_finished) +bool ArrowImpl::processRecordBatch (batch_list_t& record_batch, int num_rows, int batch_row_size_bits, bool file_finished) { bool status = false; @@ -505,38 +505,35 @@ void ArrowImpl::appendPandasMetaData (const std::shared_ptr* column, Ordering& record_batch, int num_rows, int batch_row_size_bits) +void ArrowImpl::processField (RecordObject::field_t& field, shared_ptr* column, batch_list_t& record_batch, int num_rows, int batch_row_size_bits) { - ParquetBuilder::batch_t batch; - switch(field.type) { case RecordObject::DOUBLE: { arrow::DoubleBuilder builder; (void)builder.Reserve(num_rows); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; if(field.flags & RecordObject::BATCH) { int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { - builder.UnsafeAppend((double)batch.record->getValueReal(field)); + builder.UnsafeAppend((double)batch->pri_record->getValueReal(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - float value = (float)batch.record->getValueReal(field); - for(int row = 0; row < batch.rows; row++) + float value = (float)batch->pri_record->getValueReal(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -546,28 +543,27 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - builder.UnsafeAppend((float)batch.record->getValueReal(field)); + builder.UnsafeAppend((float)batch->pri_record->getValueReal(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - float value = (float)batch.record->getValueReal(field); - for(int row = 0; row < batch.rows; row++) + float value = (float)batch->pri_record->getValueReal(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -577,28 +573,27 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - builder.UnsafeAppend((int8_t)batch.record->getValueInteger(field)); + builder.UnsafeAppend((int8_t)batch->pri_record->getValueInteger(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - int8_t value = (int8_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) + int8_t value = (int8_t)batch->pri_record->getValueInteger(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -608,28 +603,27 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - builder.UnsafeAppend((int16_t)batch.record->getValueInteger(field)); + builder.UnsafeAppend((int16_t)batch->pri_record->getValueInteger(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - int16_t value = (int16_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) + int16_t value = (int16_t)batch->pri_record->getValueInteger(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -639,28 +633,27 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - builder.UnsafeAppend((int32_t)batch.record->getValueInteger(field)); + builder.UnsafeAppend((int32_t)batch->pri_record->getValueInteger(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - int32_t value = (int32_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) + int32_t value = (int32_t)batch->pri_record->getValueInteger(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -670,28 +663,27 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - builder.UnsafeAppend((int64_t)batch.record->getValueInteger(field)); + builder.UnsafeAppend((int64_t)batch->pri_record->getValueInteger(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - int64_t value = (int64_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) + int64_t value = (int64_t)batch->pri_record->getValueInteger(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -701,28 +693,27 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - builder.UnsafeAppend((uint8_t)batch.record->getValueInteger(field)); + builder.UnsafeAppend((uint8_t)batch->pri_record->getValueInteger(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - uint8_t value = (uint8_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) + uint8_t value = (uint8_t)batch->pri_record->getValueInteger(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -732,28 +723,27 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - builder.UnsafeAppend((uint16_t)batch.record->getValueInteger(field)); + builder.UnsafeAppend((uint16_t)batch->pri_record->getValueInteger(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - uint16_t value = (uint16_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) + uint16_t value = (uint16_t)batch->pri_record->getValueInteger(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -763,28 +753,27 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - builder.UnsafeAppend((uint32_t)batch.record->getValueInteger(field)); + builder.UnsafeAppend((uint32_t)batch->pri_record->getValueInteger(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - uint32_t value = (uint32_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) + uint32_t value = (uint32_t)batch->pri_record->getValueInteger(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -794,28 +783,27 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - builder.UnsafeAppend((uint64_t)batch.record->getValueInteger(field)); + builder.UnsafeAppend((uint64_t)batch->pri_record->getValueInteger(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - uint64_t value = (uint64_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) + uint64_t value = (uint64_t)batch->pri_record->getValueInteger(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -825,28 +813,27 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - builder.UnsafeAppend((int64_t)batch.record->getValueInteger(field)); + builder.UnsafeAppend((int64_t)batch->pri_record->getValueInteger(field)); field.offset += batch_row_size_bits; } field.offset = starting_offset; } else // non-batch field { - int64_t value = (int64_t)batch.record->getValueInteger(field); - for(int row = 0; row < batch.rows; row++) + int64_t value = (int64_t)batch->pri_record->getValueInteger(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -856,15 +843,15 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrrows; row++) { - const char* str = batch.record->getValueText(field); + const char* str = batch->pri_record->getValueText(field); builder.UnsafeAppend(str, StringLib::size(str)); field.offset += batch_row_size_bits; } @@ -872,13 +859,12 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrgetValueText(field); - for(int row = 0; row < batch.rows; row++) + const char* str = batch->pri_record->getValueText(field); + for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(str, StringLib::size(str)); } } - key = record_batch.next(&batch); } (void)builder.Finish(column); break; @@ -894,10 +880,8 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptr* column, Ordering& record_batch, int batch_row_size_bits) +void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr* column, batch_list_t& record_batch, int batch_row_size_bits) { - ParquetBuilder::batch_t batch; - if(!(field.flags & RecordObject::BATCH)) { batch_row_size_bits = 0; @@ -909,21 +893,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((double)batch.record->getValueReal(field, element)); + (void)builder->Append((double)batch->pri_record->getValueReal(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -933,21 +916,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((float)batch.record->getValueReal(field, element)); + (void)builder->Append((float)batch->pri_record->getValueReal(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -957,21 +939,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((int8_t)batch.record->getValueInteger(field, element)); + (void)builder->Append((int8_t)batch->pri_record->getValueInteger(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -981,21 +962,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((int16_t)batch.record->getValueInteger(field, element)); + (void)builder->Append((int16_t)batch->pri_record->getValueInteger(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -1005,21 +985,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((int32_t)batch.record->getValueInteger(field, element)); + (void)builder->Append((int32_t)batch->pri_record->getValueInteger(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -1029,21 +1008,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((int64_t)batch.record->getValueInteger(field, element)); + (void)builder->Append((int64_t)batch->pri_record->getValueInteger(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -1053,21 +1031,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((uint8_t)batch.record->getValueInteger(field, element)); + (void)builder->Append((uint8_t)batch->pri_record->getValueInteger(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -1077,21 +1054,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((uint16_t)batch.record->getValueInteger(field, element)); + (void)builder->Append((uint16_t)batch->pri_record->getValueInteger(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -1101,21 +1077,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((uint32_t)batch.record->getValueInteger(field, element)); + (void)builder->Append((uint32_t)batch->pri_record->getValueInteger(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -1125,21 +1100,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((uint64_t)batch.record->getValueInteger(field, element)); + (void)builder->Append((uint64_t)batch->pri_record->getValueInteger(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -1149,21 +1123,20 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(arrow::timestamp(arrow::TimeUnit::NANO), arrow::default_memory_pool()); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - (void)builder->Append((int64_t)batch.record->getValueInteger(field, element)); + (void)builder->Append((int64_t)batch->pri_record->getValueInteger(field, element)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -1173,22 +1146,21 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr(); arrow::ListBuilder list_builder(arrow::default_memory_pool(), builder); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_offset = field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { (void)list_builder.Append(); for(int element = 0; element < field.elements; element++) { - const char* str = batch.record->getValueText(field, NULL, element); + const char* str = batch->pri_record->getValueText(field, NULL, element); (void)builder->Append(str, StringLib::size(str)); } field.offset += batch_row_size_bits; } field.offset = starting_offset; - key = record_batch.next(&batch); } (void)list_builder.Finish(column); break; @@ -1204,18 +1176,17 @@ void ArrowImpl::processArray (RecordObject::field_t& field, shared_ptr* column, Ordering& record_batch, int num_rows, int batch_row_size_bits) +void ArrowImpl::processGeometry (RecordObject::field_t& x_field, RecordObject::field_t& y_field, shared_ptr* column, batch_list_t& record_batch, int num_rows, int batch_row_size_bits) { - ParquetBuilder::batch_t batch; arrow::BinaryBuilder builder; (void)builder.Reserve(num_rows); (void)builder.ReserveData(num_rows * sizeof(wkbpoint_t)); - unsigned long key = record_batch.first(&batch); - while(key != (unsigned long)INVALID_KEY) + for(int i = 0; i < record_batch.length(); i++) { + ParquetBuilder::batch_t* batch = record_batch[i]; int32_t starting_x_offset = x_field.offset; int32_t starting_y_offset = y_field.offset; - for(int row = 0; row < batch.rows; row++) + for(int row = 0; row < batch->rows; row++) { wkbpoint_t point = { #ifdef __be__ @@ -1224,8 +1195,8 @@ void ArrowImpl::processGeometry (RecordObject::field_t& x_field, RecordObject::f .byteOrder = 1, #endif .wkbType = 1, - .x = batch.record->getValueReal(x_field), - .y = batch.record->getValueReal(y_field) + .x = batch->pri_record->getValueReal(x_field), + .y = batch->pri_record->getValueReal(y_field) }; (void)builder.UnsafeAppend((uint8_t*)&point, sizeof(wkbpoint_t)); if(x_field.flags & RecordObject::BATCH) x_field.offset += batch_row_size_bits; @@ -1233,7 +1204,6 @@ void ArrowImpl::processGeometry (RecordObject::field_t& x_field, RecordObject::f } x_field.offset = starting_x_offset; y_field.offset = starting_y_offset; - key = record_batch.next(&batch); } (void)builder.Finish(column); } \ No newline at end of file diff --git a/packages/arrow/ArrowImpl.h b/packages/arrow/ArrowImpl.h index f82b058f1..29aa10b75 100644 --- a/packages/arrow/ArrowImpl.h +++ b/packages/arrow/ArrowImpl.h @@ -64,6 +64,12 @@ class ArrowImpl { public: + /*-------------------------------------------------------------------- + * Types + *--------------------------------------------------------------------*/ + + typedef ParquetBuilder::batch_list_t batch_list_t; + /*-------------------------------------------------------------------- * Methods *--------------------------------------------------------------------*/ @@ -71,8 +77,8 @@ class ArrowImpl explicit ArrowImpl (ParquetBuilder* _builder); ~ArrowImpl (void); - bool processRecordBatch (Ordering& record_batch, - int num_rows, int batch_row_size_bits, bool file_finished=false); + bool processRecordBatch (batch_list_t& record_batch, int num_rows, + int batch_row_size_bits, bool file_finished=false); private: @@ -88,7 +94,7 @@ class ArrowImpl typedef List field_list_t; typedef field_list_t::Iterator field_iterator_t; - + typedef struct WKBPoint { uint8_t byteOrder; uint32_t wkbType; @@ -119,17 +125,17 @@ class ArrowImpl void appendPandasMetaData (const std::shared_ptr& metadata); void processField (RecordObject::field_t& field, shared_ptr* column, - Ordering& record_batch, + batch_list_t& record_batch, int num_rows, int batch_row_size_bits); void processArray (RecordObject::field_t& field, shared_ptr* column, - Ordering& record_batch, + batch_list_t& record_batch, int batch_row_size_bits); void processGeometry (RecordObject::field_t& x_field, RecordObject::field_t& y_field, shared_ptr* column, - Ordering& record_batch, + batch_list_t& record_batch, int num_rows, int batch_row_size_bits); }; diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index eab841e8f..7e2561d0e 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -322,40 +322,109 @@ void* ParquetBuilder::builderThread(void* parm) { /* Process Record */ if(ref.size > 0) - { - /* Get Record and Match to Type being Processed */ + { + /* Create Batch Structure */ RecordInterface* record = new RecordInterface((unsigned char*)ref.data, ref.size); - if(!StringLib::match(record->getRecordType(), builder->recType)) + batch_t* batch = new batch_t(ref, builder->inQ); + + /* Process Container Records */ + if(StringLib::match(record->getRecordType(), ContainerRecord::recType)) { + vector anc_vec; + + /* Loop Through Records in Container */ + ContainerRecord::rec_t* container = (ContainerRecord::rec_t*)record->getRecordData(); + for(uint32_t i = 0; i < container->rec_cnt; i++) + { + /* Pull Out Subrecord */ + uint8_t* buffer = (uint8_t*)container + container->entries[i].rec_offset; + int size = container->entries[i].rec_size; + RecordObject* subrec = new RecordInterface(buffer, size); + + /* Handle Supported Record Types */ + if(StringLib::match(subrec->getRecordType(), builder->recType)) + { + batch->pri_record = subrec; + } + else if(StringLib::match(subrec->getRecordType(), AncillaryFields::ancFieldRecType)) + { + anc_vec.push_back(subrec); + batch->anc_rows += 1; + } + else if(StringLib::match(subrec->getRecordType(), AncillaryFields::ancElementRecType)) + { + AncillaryFields::element_array_t* element_array = reinterpret_cast(subrec->getRecordData()); + batch->anc_rows += element_array->num_elements; + anc_vec.push_back(subrec); + } + else // ignore + { + delete subrec; // cleaned up + } + } + + /* Clean Up Container Record */ delete record; + + /* Build Ancillary Record Array */ + if(anc_vec.size() > 0) + { + batch->anc_records = new RecordObject* [anc_vec.size()]; + for(size_t i = 0; i < anc_vec.size(); i++) + { + batch->anc_records[i] = anc_vec[i]; + } + } + + /* Check If Primary Record Found + * must be after above code to populate + * ancillary record array so that they + * get freed */ + if(!batch->pri_record) + { + builder->outQ->postCopy(ref.data, ref.size); + delete batch; + continue; + } + } + else if(StringLib::match(record->getRecordType(), builder->recType)) + { + batch->pri_record = record; + } + else + { + /* Record of Non-Targeted Type - Pass Through */ builder->outQ->postCopy(ref.data, ref.size); - builder->inQ->dereference(ref); + delete record; + delete batch; continue; } /* Determine Rows in Record */ - int record_size_bytes = record->getAllocatedDataSize(); + int record_size_bytes = batch->pri_record->getAllocatedDataSize(); int batch_size_bytes = record_size_bytes - (builder->rowSizeBytes - builder->batchRowSizeBytes); - int num_rows = batch_size_bytes / builder->batchRowSizeBytes; + batch->rows = batch_size_bytes / builder->batchRowSizeBytes; + + /* Sanity Check Rows */ int left_over = batch_size_bytes % builder->batchRowSizeBytes; if(left_over > 0) { - mlog(ERROR, "Invalid record size received for %s: %d %% %d != 0", record->getRecordType(), batch_size_bytes, builder->batchRowSizeBytes); - delete record; // record is not batched, so must delete here - builder->inQ->dereference(ref); // record is not batched, so must dereference here + mlog(ERROR, "Invalid record size received for %s: %d %% %d != 0", batch->pri_record->getRecordType(), batch_size_bytes, builder->batchRowSizeBytes); + delete batch; continue; } - /* Create Batch Structure */ - batch_t batch = { - .ref = ref, - .record = record, - .rows = num_rows - }; + /* Sanity Check Number of Ancillary Fields */ + if(batch->anc_rows > 0 && batch->anc_rows != batch->rows) + { + mlog(ERROR, "Attempting to supply ancillary fields with mismatched number of rows for %s: %d != %d", batch->pri_record->getRecordType(), batch->anc_rows, batch->rows); + delete batch; + continue; + } /* Add Batch to Ordering */ - builder->recordBatch.add(row_cnt, batch); - row_cnt += num_rows; + builder->recordBatch.add(batch); + row_cnt += batch->rows; if(row_cnt >= builder->maxRowsInGroup) { bool status = builder->impl->processRecordBatch(builder->recordBatch, row_cnt, builder->batchRowSizeBytes * 8); @@ -364,7 +433,7 @@ void* ParquetBuilder::builderThread(void* parm) alert(RTE_ERROR, INFO, builder->outQ, NULL, "Failed to process record batch for %s", builder->outputPath); builder->active = false; // breaks out of loop } - builder->clearBatch(trace_id); + builder->recordBatch.clear(); row_cnt = 0; } } @@ -387,7 +456,7 @@ void* ParquetBuilder::builderThread(void* parm) /* Process Remaining Records */ bool status = builder->impl->processRecordBatch(builder->recordBatch, row_cnt, builder->batchRowSizeBytes * 8, true); if(!status) alert(RTE_ERROR, INFO, builder->outQ, NULL, "Failed to process last record batch for %s", builder->outputPath); - builder->clearBatch(trace_id); + builder->recordBatch.clear(); /* Send File to User */ const char* _path = builder->outputPath; @@ -428,24 +497,6 @@ void* ParquetBuilder::builderThread(void* parm) return NULL; } -/*---------------------------------------------------------------------------- - * clearBatch - *----------------------------------------------------------------------------*/ -void ParquetBuilder::clearBatch (uint32_t trace_id) -{ - batch_t batch; - uint32_t clear_trace_id = start_trace(INFO, trace_id, "clear_batch", "%s", "{}"); - unsigned long key = recordBatch.first(&batch); - while(key != (unsigned long)INVALID_KEY) - { - delete batch.record; - inQ->dereference(batch.ref); - key = recordBatch.next(&batch); - } - recordBatch.clear(); - stop_trace(INFO, clear_trace_id); -} - /*---------------------------------------------------------------------------- * send2S3 *----------------------------------------------------------------------------*/ diff --git a/packages/arrow/ParquetBuilder.h b/packages/arrow/ParquetBuilder.h index bbf28a3af..06d6640c0 100644 --- a/packages/arrow/ParquetBuilder.h +++ b/packages/arrow/ParquetBuilder.h @@ -95,18 +95,35 @@ class ParquetBuilder: public LuaObject * Types *--------------------------------------------------------------------*/ + struct batch_t { + Subscriber::msgRef_t ref; + Subscriber* in_q; + RecordObject* pri_record; + RecordObject** anc_records; + int rows; + int anc_rows; + batch_t(Subscriber::msgRef_t& _ref, Subscriber* _in_q): + ref(_ref), + in_q(_in_q), + pri_record(NULL), + anc_records(NULL), + rows(0), + anc_rows(0) {} + ~batch_t(void) { + in_q->dereference(ref); + delete pri_record; + for(int i = 0; i < anc_rows; i++) delete anc_records[i]; + delete [] anc_records; } + }; + + typedef List batch_list_t; + typedef struct { bool as_geo; RecordObject::field_t x_field; RecordObject::field_t y_field; } geo_data_t; - typedef struct { - Subscriber::msgRef_t ref; - RecordObject* record; - int rows; - } batch_t; - typedef struct { char filename[FILE_NAME_MAX_LEN]; long size; @@ -139,6 +156,12 @@ class ParquetBuilder: public LuaObject private: + /*-------------------------------------------------------------------- + * Constants + *--------------------------------------------------------------------*/ + + static const int EXPECTED_RECORDS_IN_BATCH = 256; + /*-------------------------------------------------------------------- * Data *--------------------------------------------------------------------*/ @@ -149,7 +172,7 @@ class ParquetBuilder: public LuaObject Subscriber* inQ; const char* recType; const char* timeKey; - Ordering recordBatch; + batch_list_t recordBatch; Publisher* outQ; int rowSizeBytes; int batchRowSizeBytes; @@ -164,15 +187,13 @@ class ParquetBuilder: public LuaObject * Methods *--------------------------------------------------------------------*/ - ParquetBuilder (lua_State* L, ArrowParms* parms, - const char* outq_name, const char* inq_name, - const char* rec_type, const char* id); - ~ParquetBuilder (void); - - static void* builderThread (void* parm); - void clearBatch (uint32_t trace_id); - bool send2S3 (const char* s3dst); - bool send2Client (void); + ParquetBuilder (lua_State* L, ArrowParms* parms, + const char* outq_name, const char* inq_name, + const char* rec_type, const char* id); + ~ParquetBuilder (void); + static void* builderThread (void* parm); + bool send2S3 (const char* s3dst); + bool send2Client (void); }; #endif /* __parquet_builder__ */ diff --git a/plugins/icesat2/plugin/AncillaryFields.cpp b/packages/core/AncillaryFields.cpp similarity index 99% rename from plugins/icesat2/plugin/AncillaryFields.cpp rename to packages/core/AncillaryFields.cpp index 629e5b814..eb572ef3c 100644 --- a/plugins/icesat2/plugin/AncillaryFields.cpp +++ b/packages/core/AncillaryFields.cpp @@ -38,7 +38,6 @@ #include #include "core.h" -#include "icesat2.h" /****************************************************************************** * STATIC DATA diff --git a/plugins/icesat2/plugin/AncillaryFields.h b/packages/core/AncillaryFields.h similarity index 96% rename from plugins/icesat2/plugin/AncillaryFields.h rename to packages/core/AncillaryFields.h index 44a7c7a48..5b8e7004e 100644 --- a/plugins/icesat2/plugin/AncillaryFields.h +++ b/packages/core/AncillaryFields.h @@ -65,14 +65,6 @@ struct AncillaryFields estimation_t estimation; } entry_t; - /* Ancillary Field Types */ - typedef enum { - PHOTON_ANC_TYPE = 0, - EXTENT_ANC_TYPE = 1, - ATL08_ANC_TYPE = 2, - ATL06_ANC_TYPE = 3 - } type_t; - /* Ancillary Field Record */ typedef struct { uint8_t anc_type; // type_t diff --git a/packages/core/CMakeLists.txt b/packages/core/CMakeLists.txt index 59b89b63d..0d747acb8 100644 --- a/packages/core/CMakeLists.txt +++ b/packages/core/CMakeLists.txt @@ -14,6 +14,7 @@ target_link_libraries (slideruleLib PUBLIC ${READLINE_LIB}) target_sources (slideruleLib PRIVATE ${CMAKE_CURRENT_LIST_DIR}/core.cpp + ${CMAKE_CURRENT_LIST_DIR}/AncillaryFields.cpp ${CMAKE_CURRENT_LIST_DIR}/Asset.cpp ${CMAKE_CURRENT_LIST_DIR}/CaptureDispatch.cpp ${CMAKE_CURRENT_LIST_DIR}/ClusterSocket.cpp @@ -70,6 +71,7 @@ target_include_directories (slideruleLib install ( FILES ${CMAKE_CURRENT_LIST_DIR}/core.h + ${CMAKE_CURRENT_LIST_DIR}/AncillaryFields.h ${CMAKE_CURRENT_LIST_DIR}/Asset.h ${CMAKE_CURRENT_LIST_DIR}/AssetIndex.h ${CMAKE_CURRENT_LIST_DIR}/CaptureDispatch.h diff --git a/packages/core/core.cpp b/packages/core/core.cpp index 135e87bc8..22f0844b8 100644 --- a/packages/core/core.cpp +++ b/packages/core/core.cpp @@ -170,6 +170,7 @@ void initcore (void) TimeLib::init(); LuaEngine::init(); ContainerRecord::init(); + AncillaryFields::init(); /* Register IO Drivers */ Asset::registerDriver("nil", Asset::IODriver::create); diff --git a/packages/core/core.h b/packages/core/core.h index 0ad85250d..921691218 100644 --- a/packages/core/core.h +++ b/packages/core/core.h @@ -38,6 +38,7 @@ #include "OsApi.h" +#include "AncillaryFields.h" #include "Asset.h" #include "AssetIndex.h" #include "CaptureDispatch.h" diff --git a/plugins/icesat2/CMakeLists.txt b/plugins/icesat2/CMakeLists.txt index 66c2b6bbb..d85987499 100644 --- a/plugins/icesat2/CMakeLists.txt +++ b/plugins/icesat2/CMakeLists.txt @@ -43,7 +43,6 @@ endif() target_sources(icesat2 PRIVATE ${CMAKE_CURRENT_LIST_DIR}/plugin/icesat2.cpp - ${CMAKE_CURRENT_LIST_DIR}/plugin/AncillaryFields.cpp ${CMAKE_CURRENT_LIST_DIR}/plugin/Atl03Reader.cpp ${CMAKE_CURRENT_LIST_DIR}/plugin/Atl03Indexer.cpp ${CMAKE_CURRENT_LIST_DIR}/plugin/Atl06Dispatch.cpp @@ -87,7 +86,6 @@ install (TARGETS icesat2 LIBRARY DESTINATION ${CONFDIR}) install ( FILES ${CMAKE_CURRENT_LIST_DIR}/plugin/icesat2.h - ${CMAKE_CURRENT_LIST_DIR}/plugin/AncillaryFields.h ${CMAKE_CURRENT_LIST_DIR}/plugin/Atl03Reader.h ${CMAKE_CURRENT_LIST_DIR}/plugin/Atl03Indexer.h ${CMAKE_CURRENT_LIST_DIR}/plugin/Atl06Dispatch.h diff --git a/plugins/icesat2/plugin/Atl03Reader.cpp b/plugins/icesat2/plugin/Atl03Reader.cpp index dd7626bc9..3cea9b0d7 100644 --- a/plugins/icesat2/plugin/Atl03Reader.cpp +++ b/plugins/icesat2/plugin/Atl03Reader.cpp @@ -1471,9 +1471,9 @@ void* Atl03Reader::subsettingThread (void* parm) { int rec_total_size = 0; reader->generateExtentRecord(extent_id, info, state, atl03, rec_list, rec_total_size); - Atl03Reader::generateAncillaryRecords(extent_id, parms->atl03_ph_fields, atl03.anc_ph_data, AncillaryFields::PHOTON_ANC_TYPE, photon_indices, rec_list, rec_total_size); - Atl03Reader::generateAncillaryRecords(extent_id, parms->atl03_geo_fields, atl03.anc_geo_data, AncillaryFields::EXTENT_ANC_TYPE, segment_indices, rec_list, rec_total_size); - Atl03Reader::generateAncillaryRecords(extent_id, parms->atl08_fields, atl08.anc_seg_data, AncillaryFields::ATL08_ANC_TYPE, atl08_indices, rec_list, rec_total_size); + Atl03Reader::generateAncillaryRecords(extent_id, parms->atl03_ph_fields, atl03.anc_ph_data, Icesat2Parms::PHOTON_ANC_TYPE, photon_indices, rec_list, rec_total_size); + Atl03Reader::generateAncillaryRecords(extent_id, parms->atl03_geo_fields, atl03.anc_geo_data, Icesat2Parms::EXTENT_ANC_TYPE, segment_indices, rec_list, rec_total_size); + Atl03Reader::generateAncillaryRecords(extent_id, parms->atl08_fields, atl08.anc_seg_data, Icesat2Parms::ATL08_ANC_TYPE, atl08_indices, rec_list, rec_total_size); /* Send Records */ if(rec_list.size() == 1) @@ -1662,7 +1662,7 @@ void Atl03Reader::generateExtentRecord (uint64_t extent_id, info_t* info, TrackS /*---------------------------------------------------------------------------- * generateAncillaryRecords *----------------------------------------------------------------------------*/ -void Atl03Reader::generateAncillaryRecords (uint64_t extent_id, AncillaryFields::list_t* field_list, H5DArrayDictionary* field_dict, AncillaryFields::type_t type, List* indices, vector& rec_list, int& total_size) +void Atl03Reader::generateAncillaryRecords (uint64_t extent_id, AncillaryFields::list_t* field_list, H5DArrayDictionary* field_dict, Icesat2Parms::anc_type_t type, List* indices, vector& rec_list, int& total_size) { if(field_list && field_dict && indices) { diff --git a/plugins/icesat2/plugin/Atl03Reader.h b/plugins/icesat2/plugin/Atl03Reader.h index ca9bb0b11..17e852477 100644 --- a/plugins/icesat2/plugin/Atl03Reader.h +++ b/plugins/icesat2/plugin/Atl03Reader.h @@ -325,7 +325,7 @@ class Atl03Reader: public LuaObject static double calculateBackground (TrackState& state, const Atl03Data& atl03); uint32_t calculateSegmentId (const TrackState& state, const Atl03Data& atl03); void generateExtentRecord (uint64_t extent_id, info_t* info, TrackState& state, const Atl03Data& atl03, vector& rec_list, int& total_size); - static void generateAncillaryRecords (uint64_t extent_id, AncillaryFields::list_t* field_list, H5DArrayDictionary* field_dict, AncillaryFields::type_t type, List* indices, vector& rec_list, int& total_size); + static void generateAncillaryRecords (uint64_t extent_id, AncillaryFields::list_t* field_list, H5DArrayDictionary* field_dict, Icesat2Parms::anc_type_t type, List* indices, vector& rec_list, int& total_size); void postRecord (RecordObject& record, stats_t& local_stats); static void parseResource (const char* resource, uint16_t& rgt, uint8_t& cycle, uint8_t& region); diff --git a/plugins/icesat2/plugin/Atl06Reader.cpp b/plugins/icesat2/plugin/Atl06Reader.cpp index e85d3635a..4c496f5e6 100644 --- a/plugins/icesat2/plugin/Atl06Reader.cpp +++ b/plugins/icesat2/plugin/Atl06Reader.cpp @@ -565,7 +565,7 @@ void* Atl06Reader::subsettingThread (void* parm) const char* field_name = parms->atl06_fields->get(i).field.c_str(); AncillaryFields::field_t field; - field.anc_type = AncillaryFields::ATL06_ANC_TYPE; + field.anc_type = Icesat2Parms::ATL06_ANC_TYPE; field.field_index = i; field.data_type = atl06.anc_data[field_name]->elementType(); atl06.anc_data[field_name]->serialize(&field.value[0], segment, 1); diff --git a/plugins/icesat2/plugin/Icesat2Parms.h b/plugins/icesat2/plugin/Icesat2Parms.h index f5b0137df..592973fdf 100644 --- a/plugins/icesat2/plugin/Icesat2Parms.h +++ b/plugins/icesat2/plugin/Icesat2Parms.h @@ -207,6 +207,14 @@ class Icesat2Parms: public NetsvcParms PHOREAL_UNSUPPORTED = 3 } phoreal_geoloc_t; + /* Ancillary Field Types */ + typedef enum { + PHOTON_ANC_TYPE = 0, + EXTENT_ANC_TYPE = 1, + ATL08_ANC_TYPE = 2, + ATL06_ANC_TYPE = 3 + } anc_type_t; + /* YAPC Settings */ typedef struct { uint8_t score; // minimum allowed weight of photon using yapc algorithm diff --git a/plugins/icesat2/plugin/icesat2.cpp b/plugins/icesat2/plugin/icesat2.cpp index 76019ac6e..c19f229d4 100644 --- a/plugins/icesat2/plugin/icesat2.cpp +++ b/plugins/icesat2/plugin/icesat2.cpp @@ -124,7 +124,6 @@ extern "C" { void initicesat2 (void) { /* Initialize Modules */ - AncillaryFields::init(); Atl03Reader::init(); Atl03Indexer::init(); Atl06Dispatch::init(); diff --git a/plugins/icesat2/plugin/icesat2.h b/plugins/icesat2/plugin/icesat2.h index 3bdd8c127..ae0dcf9c7 100644 --- a/plugins/icesat2/plugin/icesat2.h +++ b/plugins/icesat2/plugin/icesat2.h @@ -36,7 +36,6 @@ * INCLUDES ******************************************************************************/ -#include "AncillaryFields.h" #include "Atl03Reader.h" #include "Atl03Indexer.h" #include "Atl06Dispatch.h" From 6577230178ea2b617dcac17fc6ca8ab7d66d77e1 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Mon, 4 Mar 2024 13:48:04 +0000 Subject: [PATCH 34/43] in-work changes for ancillary fields --- packages/arrow/ArrowImpl.cpp | 83 ++++++++++++++++++++++++++++++- packages/arrow/ArrowImpl.h | 2 + packages/arrow/ParquetBuilder.cpp | 44 ++++++++++++---- packages/arrow/ParquetBuilder.h | 13 +++-- 4 files changed, 127 insertions(+), 15 deletions(-) diff --git a/packages/arrow/ArrowImpl.cpp b/packages/arrow/ArrowImpl.cpp index d4143377a..15c492063 100644 --- a/packages/arrow/ArrowImpl.cpp +++ b/packages/arrow/ArrowImpl.cpp @@ -81,8 +81,10 @@ bool ArrowImpl::processRecordBatch (batch_list_t& record_batch, int num_rows, in uint32_t parent_trace_id = EventLib::grabId(); uint32_t trace_id = start_trace(INFO, parent_trace_id, "process_batch", "{\"num_rows\": %d}", num_rows); - /* Loop Through Fields in Schema */ + /* Allocate Columns for this Batch */ vector> columns; + + /* Loop Through Fields in Primary Record */ for(int i = 0; i < fieldIterator->length; i++) { uint32_t field_trace_id = start_trace(INFO, trace_id, "append_field", "{\"field\": %d}", i); @@ -108,6 +110,12 @@ bool ArrowImpl::processRecordBatch (batch_list_t& record_batch, int num_rows, in stop_trace(INFO, geo_trace_id); } + /* Add Ancillary Columns */ + if(parquetBuilder->getHasAncillary()) + { + processAncillary(columns, record_batch); + } + /* Create Parquet Writer (on first time) */ if(firstTime) { @@ -1206,4 +1214,75 @@ void ArrowImpl::processGeometry (RecordObject::field_t& x_field, RecordObject::f y_field.offset = starting_y_offset; } (void)builder.Finish(column); -} \ No newline at end of file +} + +/*---------------------------------------------------------------------------- +* processGeometry +*----------------------------------------------------------------------------*/ +void ArrowImpl::processAncillary (vector>& columns, batch_list_t& record_batch) +{ + Dictionary> column_table; + vector& ancillary_fields = parquetBuilder->getParms()->ancillary_fields; + + +// create a dictionary of vectors of field references +// the key is the field name +// then loop through the ancillary_fields vector for each field name there +// look it up in the dictionary +// and loop through all of the fields for that field name +// at that point it will have the same type so a builder can be created + + + for(int i = 0; i < record_batch.length(); i++) + { + ParquetBuilder::batch_t* batch = record_batch[i]; + for(int j = 0; j < batch->anc_fields; j++) + { + AncillaryFields::field_array_t* field_array = reinterpret_cast(batch->anc_records[j]->getRecordData()); + for(int k = 0; k < field_array->num_fields; k++) + { + AncillaryFields::field_t& field = field_array->fields[k]; + //field.data_type + + /* Get Name */ + if(field.field_index < parquetBuilder->getParms()->ancillary_fields.size()) + { + const char* name = parquetBuilder->getParms()->ancillary_fields[field.field_index].c_str(); + } + else + { + throw RunTimeException(CRITICAL, RTE_ERROR, "Invalid field index: %d", field.field_index); + } + + } + } + + arrow::DoubleBuilder builder; + (void)builder.Reserve(num_rows); + for(int i = 0; i < record_batch.length(); i++) + { + if(field.flags & RecordObject::BATCH) + { + int32_t starting_offset = field.offset; + for(int row = 0; row < batch->rows; row++) + { + builder.UnsafeAppend((double)batch->pri_record->getValueReal(field)); + field.offset += batch_row_size_bits; + } + field.offset = starting_offset; + } + else // non-batch field + { + float value = (float)batch->pri_record->getValueReal(field); + for(int row = 0; row < batch->rows; row++) + { + builder.UnsafeAppend(value); + } + } + } + (void)builder.Finish(column); + + + + columns.push_back(column); +} diff --git a/packages/arrow/ArrowImpl.h b/packages/arrow/ArrowImpl.h index 29aa10b75..ac39a3163 100644 --- a/packages/arrow/ArrowImpl.h +++ b/packages/arrow/ArrowImpl.h @@ -138,6 +138,8 @@ class ArrowImpl batch_list_t& record_batch, int num_rows, int batch_row_size_bits); + void processAncillary (vector>& columns, + batch_list_t& record_batch); }; #endif /* __arrow_impl__ */ diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index 7e2561d0e..5ba38fb34 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -168,6 +168,22 @@ RecordObject::field_t& ParquetBuilder::getYField (void) return geoData.y_field; } +/*---------------------------------------------------------------------------- + * getHasAncillary + *----------------------------------------------------------------------------*/ +bool ParquetBuilder::getHasAncillary (void) +{ + return hasAncillary; +} + +/*---------------------------------------------------------------------------- + * getParms + *----------------------------------------------------------------------------*/ +ArrowParms* ParquetBuilder::getParms (void) +{ + return parms; +} + /****************************************************************************** * PRIVATE METHODS *******************************************************************************/ @@ -179,7 +195,8 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, const char* outq_name, const char* inq_name, const char* rec_type, const char* id): LuaObject(L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), - parms(_parms) + parms(_parms), + hasAncillary(false) { assert(_parms); assert(outq_name); @@ -349,13 +366,13 @@ void* ParquetBuilder::builderThread(void* parm) else if(StringLib::match(subrec->getRecordType(), AncillaryFields::ancFieldRecType)) { anc_vec.push_back(subrec); - batch->anc_rows += 1; + batch->anc_fields += 1; } else if(StringLib::match(subrec->getRecordType(), AncillaryFields::ancElementRecType)) { - AncillaryFields::element_array_t* element_array = reinterpret_cast(subrec->getRecordData()); - batch->anc_rows += element_array->num_elements; anc_vec.push_back(subrec); + AncillaryFields::element_array_t* element_array = reinterpret_cast(subrec->getRecordData()); + batch->anc_elements += element_array->num_elements; } else // ignore { @@ -367,10 +384,11 @@ void* ParquetBuilder::builderThread(void* parm) delete record; /* Build Ancillary Record Array */ - if(anc_vec.size() > 0) + batch->num_anc_recs = anc_vec.size(); + if(batch->num_anc_recs > 0) { - batch->anc_records = new RecordObject* [anc_vec.size()]; - for(size_t i = 0; i < anc_vec.size(); i++) + batch->anc_records = new RecordObject* [batch->num_anc_recs]; + for(size_t i = 0; i < batch->num_anc_recs; i++) { batch->anc_records[i] = anc_vec[i]; } @@ -386,6 +404,11 @@ void* ParquetBuilder::builderThread(void* parm) delete batch; continue; } + else + { + /* Ancillary Data Present */ + builder->hasAncillary = true; + } } else if(StringLib::match(record->getRecordType(), builder->recType)) { @@ -414,10 +437,11 @@ void* ParquetBuilder::builderThread(void* parm) continue; } - /* Sanity Check Number of Ancillary Fields */ - if(batch->anc_rows > 0 && batch->anc_rows != batch->rows) + /* Sanity Check Number of Ancillary Rows */ + if((batch->anc_fields > 0 && batch->anc_fields != batch->rows) || + (batch->anc_elements > 0 && batch->anc_elements != batch->rows)) { - mlog(ERROR, "Attempting to supply ancillary fields with mismatched number of rows for %s: %d != %d", batch->pri_record->getRecordType(), batch->anc_rows, batch->rows); + mlog(ERROR, "Attempting to supply ancillary data with mismatched number of rows for %s: %d,%d != %d", batch->pri_record->getRecordType(), batch->anc_fields, batch->anc_elements, batch->rows); delete batch; continue; } diff --git a/packages/arrow/ParquetBuilder.h b/packages/arrow/ParquetBuilder.h index 06d6640c0..dec49aedd 100644 --- a/packages/arrow/ParquetBuilder.h +++ b/packages/arrow/ParquetBuilder.h @@ -101,18 +101,22 @@ class ParquetBuilder: public LuaObject RecordObject* pri_record; RecordObject** anc_records; int rows; - int anc_rows; + int num_anc_recs; + int anc_fields; + int anc_elements; batch_t(Subscriber::msgRef_t& _ref, Subscriber* _in_q): ref(_ref), in_q(_in_q), pri_record(NULL), anc_records(NULL), rows(0), - anc_rows(0) {} + num_anc_recs(0), + anc_fields(0), + anc_elements(0) {} ~batch_t(void) { in_q->dereference(ref); delete pri_record; - for(int i = 0; i < anc_rows; i++) delete anc_records[i]; + for(int i = 0; i < num_anc_recs; i++) delete anc_records[i]; delete [] anc_records; } }; @@ -153,6 +157,8 @@ class ParquetBuilder: public LuaObject bool getAsGeo (void); RecordObject::field_t& getXField (void); RecordObject::field_t& getYField (void); + bool getHasAncillary (void); + ArrowParms* getParms (void); private: @@ -173,6 +179,7 @@ class ParquetBuilder: public LuaObject const char* recType; const char* timeKey; batch_list_t recordBatch; + bool hasAncillary; Publisher* outQ; int rowSizeBytes; int batchRowSizeBytes; From 02511679c3bb7c449033e80b254738aae91e8658 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Tue, 5 Mar 2024 12:42:35 +0000 Subject: [PATCH 35/43] working functionality for ancillary fields in parquet files --- packages/arrow/ArrowImpl.cpp | 586 ++++++++++++++++++++++++++---- packages/arrow/ArrowImpl.h | 5 +- packages/arrow/ParquetBuilder.cpp | 36 +- packages/arrow/ParquetBuilder.h | 6 +- packages/core/AncillaryFields.cpp | 13 + packages/core/AncillaryFields.h | 1 + 6 files changed, 551 insertions(+), 96 deletions(-) diff --git a/packages/arrow/ArrowImpl.cpp b/packages/arrow/ArrowImpl.cpp index 15c492063..ff05a072f 100644 --- a/packages/arrow/ArrowImpl.cpp +++ b/packages/arrow/ArrowImpl.cpp @@ -29,6 +29,13 @@ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +/* + * The order of the columns in the parquet file are: + * - Fields from the primary record + * - Geometry + * - Ancillary fields + */ + /****************************************************************************** * INCLUDES ******************************************************************************/ @@ -53,13 +60,11 @@ ArrowImpl::ArrowImpl (ParquetBuilder* _builder): parquetBuilder(_builder), schema(NULL), fieldList(LIST_BLOCK_SIZE), - fieldIterator(NULL), firstTime(true) { /* Build Field List and Iterator */ buildFieldList(parquetBuilder->getRecType(), 0, 0); if(parquetBuilder->getAsGeo()) fieldVector.push_back(arrow::field("geometry", arrow::binary())); - fieldIterator = new field_iterator_t(fieldList); } /*---------------------------------------------------------------------------- @@ -67,7 +72,6 @@ ArrowImpl::ArrowImpl (ParquetBuilder* _builder): *----------------------------------------------------------------------------*/ ArrowImpl::~ArrowImpl (void) { - delete fieldIterator; } /*---------------------------------------------------------------------------- @@ -85,10 +89,10 @@ bool ArrowImpl::processRecordBatch (batch_list_t& record_batch, int num_rows, in vector> columns; /* Loop Through Fields in Primary Record */ - for(int i = 0; i < fieldIterator->length; i++) + for(int i = 0; i < fieldList.length(); i++) { uint32_t field_trace_id = start_trace(INFO, trace_id, "append_field", "{\"field\": %d}", i); - RecordObject::field_t field = (*fieldIterator)[i]; + RecordObject::field_t& field = fieldList[i]; /* Build Column */ shared_ptr column; @@ -111,10 +115,8 @@ bool ArrowImpl::processRecordBatch (batch_list_t& record_batch, int num_rows, in } /* Add Ancillary Columns */ - if(parquetBuilder->getHasAncillary()) - { - processAncillary(columns, record_batch); - } + if(parquetBuilder->hasAncFields()) processAncillaryFields(columns, record_batch); + if(parquetBuilder->hasAncElements()) processAncillaryElements(columns, record_batch); /* Create Parquet Writer (on first time) */ if(firstTime) @@ -427,47 +429,38 @@ void ArrowImpl::appendPandasMetaData (const std::shared_ptrfield_names()) + int column_index = 0; + int num_columns = schema->fields().size(); + for(const shared_ptr& field: schema->fields()) { + const string& field_name = field->name(); + const shared_ptr& field_type = field->type(); + /* Initialize Column String */ string columnstr(R"json({"name": "_NAME_", "field_name": "_NAME_", "pandas_type": "_PTYPE_", "numpy_type": "_NTYPE_", "metadata": null})json"); const char* pandas_type = ""; const char* numpy_type = ""; bool is_last_entry = false; - if(index < fieldIterator->length) - { - /* Add Column from Field List */ - RecordObject::field_t field = (*fieldIterator)[index++]; - switch(field.type) - { - case RecordObject::DOUBLE: pandas_type = "float64"; numpy_type = "float64"; break; - case RecordObject::FLOAT: pandas_type = "float32"; numpy_type = "float32"; break; - case RecordObject::INT8: pandas_type = "int8"; numpy_type = "int8"; break; - case RecordObject::INT16: pandas_type = "int16"; numpy_type = "int16"; break; - case RecordObject::INT32: pandas_type = "int32"; numpy_type = "int32"; break; - case RecordObject::INT64: pandas_type = "int64"; numpy_type = "int64"; break; - case RecordObject::UINT8: pandas_type = "uint8"; numpy_type = "uint8"; break; - case RecordObject::UINT16: pandas_type = "uint16"; numpy_type = "uint16"; break; - case RecordObject::UINT32: pandas_type = "uint32"; numpy_type = "uint32"; break; - case RecordObject::UINT64: pandas_type = "uint64"; numpy_type = "uint64"; break; - case RecordObject::TIME8: pandas_type = "datetime"; numpy_type = "datetime64[ns]"; break; - case RecordObject::STRING: pandas_type = "bytes"; numpy_type = "object"; break; - default: pandas_type = "bytes"; numpy_type = "object"; break; - } - - /* Mark Last Column */ - if(!parquetBuilder->getAsGeo() && (index == fieldIterator->length)) - { - is_last_entry = true; - } - } - else if(parquetBuilder->getAsGeo() && StringLib::match(field_name.c_str(), "geometry")) + if (field_type->Equals(arrow::float64())) { pandas_type = "float64"; numpy_type = "float64"; } + else if (field_type->Equals(arrow::float32())) { pandas_type = "float32"; numpy_type = "float32"; } + else if (field_type->Equals(arrow::int8())) { pandas_type = "int8"; numpy_type = "int8"; } + else if (field_type->Equals(arrow::int16())) { pandas_type = "int16"; numpy_type = "int16"; } + else if (field_type->Equals(arrow::int32())) { pandas_type = "int32"; numpy_type = "int32"; } + else if (field_type->Equals(arrow::int64())) { pandas_type = "int64"; numpy_type = "int64"; } + else if (field_type->Equals(arrow::uint8())) { pandas_type = "uint8"; numpy_type = "uint8"; } + else if (field_type->Equals(arrow::uint16())) { pandas_type = "uint16"; numpy_type = "uint16"; } + else if (field_type->Equals(arrow::uint32())) { pandas_type = "uint32"; numpy_type = "uint32"; } + else if (field_type->Equals(arrow::uint64())) { pandas_type = "uint64"; numpy_type = "uint64"; } + else if (field_type->Equals(arrow::timestamp(arrow::TimeUnit::NANO))) + { pandas_type = "datetime"; numpy_type = "datetime64[ns]"; } + else if (field_type->Equals(arrow::utf8())) { pandas_type = "bytes"; numpy_type = "object"; } + else if (field_type->Equals(arrow::binary())) { pandas_type = "bytes"; numpy_type = "object"; } + else { pandas_type = "bytes"; numpy_type = "object"; } + + /* Mark Last Column */ + if(++column_index == num_columns) { - /* Add Column for Geometry */ - pandas_type = "bytes"; - numpy_type = "object"; is_last_entry = true; } @@ -536,7 +529,7 @@ void ArrowImpl::processField (RecordObject::field_t& field, shared_ptrpri_record->getValueReal(field); + double value = (double)batch->pri_record->getValueReal(field); for(int row = 0; row < batch->rows; row++) { builder.UnsafeAppend(value); @@ -1217,72 +1210,509 @@ void ArrowImpl::processGeometry (RecordObject::field_t& x_field, RecordObject::f } /*---------------------------------------------------------------------------- -* processGeometry +* processAncillaryFields *----------------------------------------------------------------------------*/ -void ArrowImpl::processAncillary (vector>& columns, batch_list_t& record_batch) +void ArrowImpl::processAncillaryFields (vector>& columns, batch_list_t& record_batch) { - Dictionary> column_table; vector& ancillary_fields = parquetBuilder->getParms()->ancillary_fields; + Dictionary> field_table; + Dictionary field_type_table; + /* Initialize Field Table */ + for(size_t i = 0; i < ancillary_fields.size(); i++) + { + const char* name = ancillary_fields[i].c_str(); + vector field_vec; + field_table.add(name, field_vec); + } -// create a dictionary of vectors of field references -// the key is the field name -// then loop through the ancillary_fields vector for each field name there -// look it up in the dictionary -// and loop through all of the fields for that field name -// at that point it will have the same type so a builder can be created - - + /* Populate Field Table */ for(int i = 0; i < record_batch.length(); i++) { ParquetBuilder::batch_t* batch = record_batch[i]; - for(int j = 0; j < batch->anc_fields; j++) + + /* Loop through Ancillary Fields */ + for(int j = 0; j < batch->num_anc_recs; j++) { AncillaryFields::field_array_t* field_array = reinterpret_cast(batch->anc_records[j]->getRecordData()); - for(int k = 0; k < field_array->num_fields; k++) + for(uint32_t k = 0; k < field_array->num_fields; k++) { AncillaryFields::field_t& field = field_array->fields[k]; - //field.data_type - + const char* name = NULL; + /* Get Name */ - if(field.field_index < parquetBuilder->getParms()->ancillary_fields.size()) + if(field.field_index < ancillary_fields.size()) name = ancillary_fields[field.field_index].c_str(); + else throw RunTimeException(CRITICAL, RTE_ERROR, "Invalid field index: %d", field.field_index); + + /* Add to Field Table */ + field_table[name].push_back(&field); + field_type_table.add(name, static_cast(field.data_type), false); + } + } + } + + /* Loop Through Fields */ + for(size_t i = 0; i < ancillary_fields.size(); i++) + { + const char* name = ancillary_fields[i].c_str(); + vector& field_vec = field_table[name]; + RecordObject::fieldType_t type = field_type_table[name]; + int num_rows = field_vec.size(); + + /* Populate Schema */ + if(firstTime) + { + switch(type) + { + case RecordObject::INT8: fieldVector.push_back(arrow::field(name, arrow::int8())); break; + case RecordObject::INT16: fieldVector.push_back(arrow::field(name, arrow::int16())); break; + case RecordObject::INT32: fieldVector.push_back(arrow::field(name, arrow::int32())); break; + case RecordObject::INT64: fieldVector.push_back(arrow::field(name, arrow::int64())); break; + case RecordObject::UINT8: fieldVector.push_back(arrow::field(name, arrow::uint8())); break; + case RecordObject::UINT16: fieldVector.push_back(arrow::field(name, arrow::uint16())); break; + case RecordObject::UINT32: fieldVector.push_back(arrow::field(name, arrow::uint32())); break; + case RecordObject::UINT64: fieldVector.push_back(arrow::field(name, arrow::uint64())); break; + case RecordObject::FLOAT: fieldVector.push_back(arrow::field(name, arrow::float32())); break; + case RecordObject::DOUBLE: fieldVector.push_back(arrow::field(name, arrow::float64())); break; + case RecordObject::TIME8: fieldVector.push_back(arrow::field(name, arrow::timestamp(arrow::TimeUnit::NANO))); break; + default: break; + } + } + + /* Populate Column */ + shared_ptr column; + switch(type) + { + case RecordObject::DOUBLE: + { + arrow::DoubleBuilder builder; + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) + { + AncillaryFields::field_t* field = field_vec[i]; + double* val_ptr = AncillaryFields::getValueAsDouble(field->value); + builder.UnsafeAppend((double)*val_ptr); + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::FLOAT: + { + arrow::FloatBuilder builder; + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) + { + AncillaryFields::field_t* field = field_vec[i]; + float* val_ptr = AncillaryFields::getValueAsFloat(field->value); + builder.UnsafeAppend((float)*val_ptr); + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::INT8: + { + arrow::Int8Builder builder; + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) + { + AncillaryFields::field_t* field = field_vec[i]; + int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); + builder.UnsafeAppend((int8_t)*val_ptr); + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::INT16: + { + arrow::Int16Builder builder; + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) + { + AncillaryFields::field_t* field = field_vec[i]; + int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); + builder.UnsafeAppend((int16_t)*val_ptr); + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::INT32: + { + arrow::Int32Builder builder; + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) + { + AncillaryFields::field_t* field = field_vec[i]; + int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); + builder.UnsafeAppend((int32_t)*val_ptr); + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::INT64: + { + arrow::Int64Builder builder; + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) + { + AncillaryFields::field_t* field = field_vec[i]; + int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); + builder.UnsafeAppend((int64_t)*val_ptr); + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::UINT8: + { + arrow::UInt8Builder builder; + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) { - const char* name = parquetBuilder->getParms()->ancillary_fields[field.field_index].c_str(); + AncillaryFields::field_t* field = field_vec[i]; + int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); + builder.UnsafeAppend((uint8_t)*val_ptr); } - else + (void)builder.Finish(&column); + break; + } + + case RecordObject::UINT16: + { + arrow::UInt16Builder builder; + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) { - throw RunTimeException(CRITICAL, RTE_ERROR, "Invalid field index: %d", field.field_index); + AncillaryFields::field_t* field = field_vec[i]; + int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); + builder.UnsafeAppend((uint16_t)*val_ptr); } + (void)builder.Finish(&column); + break; + } + case RecordObject::UINT32: + { + arrow::UInt32Builder builder; + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) + { + AncillaryFields::field_t* field = field_vec[i]; + int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); + builder.UnsafeAppend((uint32_t)*val_ptr); + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::UINT64: + { + arrow::UInt64Builder builder; + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) + { + AncillaryFields::field_t* field = field_vec[i]; + int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); + builder.UnsafeAppend((uint64_t)*val_ptr); + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::TIME8: + { + arrow::TimestampBuilder builder(arrow::timestamp(arrow::TimeUnit::NANO), arrow::default_memory_pool()); + (void)builder.Reserve(num_rows); + for(int j = 0; j < num_rows; j++) + { + AncillaryFields::field_t* field = field_vec[i]; + int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); + builder.UnsafeAppend((int64_t)*val_ptr); + } + (void)builder.Finish(&column); + break; + } + + default: + { + break; } } - arrow::DoubleBuilder builder; - (void)builder.Reserve(num_rows); - for(int i = 0; i < record_batch.length(); i++) + /* Add to Columns */ + columns.push_back(column); + } +} + +/*---------------------------------------------------------------------------- +* processAncillaryElements +*----------------------------------------------------------------------------*/ +void ArrowImpl::processAncillaryElements (vector>& columns, batch_list_t& record_batch) +{ + int num_rows = 0; + vector& ancillary_fields = parquetBuilder->getParms()->ancillary_fields; + Dictionary> element_table; + Dictionary element_type_table; + + /* Initialize Field Table */ + for(size_t i = 0; i < ancillary_fields.size(); i++) + { + const char* name = ancillary_fields[i].c_str(); + vector element_vec; + element_table.add(name, element_vec); + } + + /* Populate Field Table */ + for(int i = 0; i < record_batch.length(); i++) + { + ParquetBuilder::batch_t* batch = record_batch[i]; + + /* Loop through Ancillary Elements */ + for(int j = 0; j < batch->num_anc_recs; j++) + { + AncillaryFields::element_array_t* element_array = reinterpret_cast(batch->anc_records[j]->getRecordData()); + + /* Get Name */ + const char* name = NULL; + if(element_array->field_index < ancillary_fields.size()) name = ancillary_fields[element_array->field_index].c_str(); + else throw RunTimeException(CRITICAL, RTE_ERROR, "Invalid field index: %d", element_array->field_index); + + /* Add to Element Table */ + element_table[name].push_back(element_array); + element_type_table.add(name, static_cast(element_array->data_type), false); + num_rows += element_array->num_elements; + } + } + + /* Loop Through Fields */ + for(size_t i = 0; i < ancillary_fields.size(); i++) + { + const char* name = ancillary_fields[i].c_str(); + vector& element_vec = element_table[name]; + RecordObject::fieldType_t type = element_type_table[name]; + + /* Populate Schema */ + if(firstTime) + { + switch(type) { - if(field.flags & RecordObject::BATCH) + case RecordObject::INT8: fieldVector.push_back(arrow::field(name, arrow::int8())); break; + case RecordObject::INT16: fieldVector.push_back(arrow::field(name, arrow::int16())); break; + case RecordObject::INT32: fieldVector.push_back(arrow::field(name, arrow::int32())); break; + case RecordObject::INT64: fieldVector.push_back(arrow::field(name, arrow::int64())); break; + case RecordObject::UINT8: fieldVector.push_back(arrow::field(name, arrow::uint8())); break; + case RecordObject::UINT16: fieldVector.push_back(arrow::field(name, arrow::uint16())); break; + case RecordObject::UINT32: fieldVector.push_back(arrow::field(name, arrow::uint32())); break; + case RecordObject::UINT64: fieldVector.push_back(arrow::field(name, arrow::uint64())); break; + case RecordObject::FLOAT: fieldVector.push_back(arrow::field(name, arrow::float32())); break; + case RecordObject::DOUBLE: fieldVector.push_back(arrow::field(name, arrow::float64())); break; + case RecordObject::TIME8: fieldVector.push_back(arrow::field(name, arrow::timestamp(arrow::TimeUnit::NANO))); break; + default: break; + } + } + + /* Populate Column */ + shared_ptr column; + switch(type) + { + case RecordObject::DOUBLE: + { + arrow::DoubleBuilder builder; + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) { - int32_t starting_offset = field.offset; - for(int row = 0; row < batch->rows; row++) + AncillaryFields::element_array_t* element_array = element_vec[j]; + double* src = AncillaryFields::getValueAsDouble(&element_array->data[0]); + for(uint32_t k = 0; k < element_array->num_elements; k++) { - builder.UnsafeAppend((double)batch->pri_record->getValueReal(field)); - field.offset += batch_row_size_bits; + builder.UnsafeAppend(src[k]); } - field.offset = starting_offset; } - else // non-batch field + (void)builder.Finish(&column); + break; + } + + case RecordObject::FLOAT: + { + arrow::FloatBuilder builder; + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) { - float value = (float)batch->pri_record->getValueReal(field); - for(int row = 0; row < batch->rows; row++) + AncillaryFields::element_array_t* element_array = element_vec[j]; + float* src = AncillaryFields::getValueAsFloat(&element_array->data[0]); + for(uint32_t k = 0; k < element_array->num_elements; k++) { - builder.UnsafeAppend(value); + builder.UnsafeAppend(src[k]); } } + (void)builder.Finish(&column); + break; } - (void)builder.Finish(column); + case RecordObject::INT8: + { + arrow::Int8Builder builder; + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) + { + AncillaryFields::element_array_t* element_array = element_vec[j]; + int8_t* src = (int8_t*)&element_array->data[0]; + for(uint32_t k = 0; k < element_array->num_elements; k++) + { + builder.UnsafeAppend(src[k]); + } + } + (void)builder.Finish(&column); + break; + } - - columns.push_back(column); + case RecordObject::INT16: + { + arrow::Int16Builder builder; + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) + { + AncillaryFields::element_array_t* element_array = element_vec[j]; + int16_t* src = (int16_t*)&element_array->data[0]; + for(uint32_t k = 0; k < element_array->num_elements; k++) + { + builder.UnsafeAppend(src[k]); + } + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::INT32: + { + arrow::Int32Builder builder; + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) + { + AncillaryFields::element_array_t* element_array = element_vec[j]; + int32_t* src = (int32_t*)&element_array->data[0]; + for(uint32_t k = 0; k < element_array->num_elements; k++) + { + builder.UnsafeAppend(src[k]); + } + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::INT64: + { + arrow::Int64Builder builder; + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) + { + AncillaryFields::element_array_t* element_array = element_vec[j]; + int64_t* src = (int64_t*)&element_array->data[0]; + for(uint32_t k = 0; k < element_array->num_elements; k++) + { + builder.UnsafeAppend(src[k]); + } + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::UINT8: + { + arrow::UInt8Builder builder; + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) + { + AncillaryFields::element_array_t* element_array = element_vec[j]; + uint8_t* src = (uint8_t*)&element_array->data[0]; + for(uint32_t k = 0; k < element_array->num_elements; k++) + { + builder.UnsafeAppend(src[k]); + } + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::UINT16: + { + arrow::UInt16Builder builder; + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) + { + AncillaryFields::element_array_t* element_array = element_vec[j]; + uint16_t* src = (uint16_t*)&element_array->data[0]; + for(uint32_t k = 0; k < element_array->num_elements; k++) + { + builder.UnsafeAppend(src[k]); + } + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::UINT32: + { + arrow::UInt32Builder builder; + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) + { + AncillaryFields::element_array_t* element_array = element_vec[j]; + uint32_t* src = (uint32_t*)&element_array->data[0]; + for(uint32_t k = 0; k < element_array->num_elements; k++) + { + builder.UnsafeAppend(src[k]); + } + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::UINT64: + { + arrow::UInt64Builder builder; + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) + { + AncillaryFields::element_array_t* element_array = element_vec[j]; + uint64_t* src = (uint64_t*)&element_array->data[0]; + for(uint32_t k = 0; k < element_array->num_elements; k++) + { + builder.UnsafeAppend(src[k]); + } + } + (void)builder.Finish(&column); + break; + } + + case RecordObject::TIME8: + { + arrow::TimestampBuilder builder(arrow::timestamp(arrow::TimeUnit::NANO), arrow::default_memory_pool()); + (void)builder.Reserve(num_rows); + for(size_t j = 0; j < element_vec.size(); j++) + { + AncillaryFields::element_array_t* element_array = element_vec[j]; + int64_t* src = (int64_t*)&element_array->data[0]; + for(uint32_t k = 0; k < element_array->num_elements; k++) + { + builder.UnsafeAppend(src[k]); + } + } + (void)builder.Finish(&column); + break; + } + + default: + { + break; + } + } + + /* Add to Columns */ + columns.push_back(column); + } } diff --git a/packages/arrow/ArrowImpl.h b/packages/arrow/ArrowImpl.h index ac39a3163..d8237e953 100644 --- a/packages/arrow/ArrowImpl.h +++ b/packages/arrow/ArrowImpl.h @@ -111,7 +111,6 @@ class ArrowImpl unique_ptr parquetWriter; vector> fieldVector; field_list_t fieldList; - field_iterator_t* fieldIterator; bool firstTime; /*-------------------------------------------------------------------- @@ -138,7 +137,9 @@ class ArrowImpl batch_list_t& record_batch, int num_rows, int batch_row_size_bits); - void processAncillary (vector>& columns, + void processAncillaryFields (vector>& columns, + batch_list_t& record_batch); + void processAncillaryElements(vector>& columns, batch_list_t& record_batch); }; diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index 5ba38fb34..6821ee57c 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -169,19 +169,27 @@ RecordObject::field_t& ParquetBuilder::getYField (void) } /*---------------------------------------------------------------------------- - * getHasAncillary + * getParms *----------------------------------------------------------------------------*/ -bool ParquetBuilder::getHasAncillary (void) +ArrowParms* ParquetBuilder::getParms (void) { - return hasAncillary; + return parms; } /*---------------------------------------------------------------------------- - * getParms + * hasAncFields *----------------------------------------------------------------------------*/ -ArrowParms* ParquetBuilder::getParms (void) +bool ParquetBuilder::hasAncFields (void) { - return parms; + return hasAncillaryFields; +} + +/*---------------------------------------------------------------------------- + * hasAncElements + *----------------------------------------------------------------------------*/ +bool ParquetBuilder::hasAncElements (void) +{ + return hasAncillaryElements; } /****************************************************************************** @@ -196,7 +204,8 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, const char* rec_type, const char* id): LuaObject(L, OBJECT_TYPE, LUA_META_NAME, LUA_META_TABLE), parms(_parms), - hasAncillary(false) + hasAncillaryFields(false), + hasAncillaryElements(false) { assert(_parms); assert(outq_name); @@ -363,7 +372,7 @@ void* ParquetBuilder::builderThread(void* parm) { batch->pri_record = subrec; } - else if(StringLib::match(subrec->getRecordType(), AncillaryFields::ancFieldRecType)) + else if(StringLib::match(subrec->getRecordType(), AncillaryFields::ancFieldArrayRecType)) { anc_vec.push_back(subrec); batch->anc_fields += 1; @@ -388,7 +397,7 @@ void* ParquetBuilder::builderThread(void* parm) if(batch->num_anc_recs > 0) { batch->anc_records = new RecordObject* [batch->num_anc_recs]; - for(size_t i = 0; i < batch->num_anc_recs; i++) + for(int i = 0; i < batch->num_anc_recs; i++) { batch->anc_records[i] = anc_vec[i]; } @@ -404,11 +413,6 @@ void* ParquetBuilder::builderThread(void* parm) delete batch; continue; } - else - { - /* Ancillary Data Present */ - builder->hasAncillary = true; - } } else if(StringLib::match(record->getRecordType(), builder->recType)) { @@ -446,6 +450,10 @@ void* ParquetBuilder::builderThread(void* parm) continue; } + /* Set Ancillary Flags */ + if(batch->anc_fields > 0) builder->hasAncillaryFields = true; + if(batch->anc_elements > 0) builder->hasAncillaryElements = true; + /* Add Batch to Ordering */ builder->recordBatch.add(batch); row_cnt += batch->rows; diff --git a/packages/arrow/ParquetBuilder.h b/packages/arrow/ParquetBuilder.h index dec49aedd..445fd4b25 100644 --- a/packages/arrow/ParquetBuilder.h +++ b/packages/arrow/ParquetBuilder.h @@ -157,8 +157,9 @@ class ParquetBuilder: public LuaObject bool getAsGeo (void); RecordObject::field_t& getXField (void); RecordObject::field_t& getYField (void); - bool getHasAncillary (void); ArrowParms* getParms (void); + bool hasAncFields (void); + bool hasAncElements (void); private: @@ -179,7 +180,8 @@ class ParquetBuilder: public LuaObject const char* recType; const char* timeKey; batch_list_t recordBatch; - bool hasAncillary; + bool hasAncillaryFields; + bool hasAncillaryElements; Publisher* outQ; int rowSizeBytes; int batchRowSizeBytes; diff --git a/packages/core/AncillaryFields.cpp b/packages/core/AncillaryFields.cpp index eb572ef3c..dbc2d7685 100644 --- a/packages/core/AncillaryFields.cpp +++ b/packages/core/AncillaryFields.cpp @@ -310,6 +310,19 @@ float* AncillaryFields::getValueAsFloat (uint8_t* buffer) return cast.fptr; } +/*---------------------------------------------------------------------------- + * getValueAsInteger + *----------------------------------------------------------------------------*/ +int64_t* AncillaryFields::getValueAsInteger (uint8_t* buffer) +{ + union { + int64_t* lptr; + uint8_t* iptr; + } cast; + cast.iptr = buffer; + return cast.lptr; +} + /*---------------------------------------------------------------------------- * createFieldArrayRecord *----------------------------------------------------------------------------*/ diff --git a/packages/core/AncillaryFields.h b/packages/core/AncillaryFields.h index 5b8e7004e..720d6ecdf 100644 --- a/packages/core/AncillaryFields.h +++ b/packages/core/AncillaryFields.h @@ -120,6 +120,7 @@ struct AncillaryFields static void setValueAsInteger (field_t* field, int64_t value); static double* getValueAsDouble (uint8_t* buffer); static float* getValueAsFloat (uint8_t* buffer); + static int64_t* getValueAsInteger (uint8_t* buffer); static RecordObject* createFieldArrayRecord (uint64_t extent_id, vector& field_vec); }; From 2f27e6971e73217b1e467b49ddfe4f7be3d62043 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Tue, 5 Mar 2024 15:53:53 +0000 Subject: [PATCH 36/43] fixed comment in raster sampler --- packages/geo/RasterSampler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/geo/RasterSampler.cpp b/packages/geo/RasterSampler.cpp index 2db790fd1..518299a57 100644 --- a/packages/geo/RasterSampler.cpp +++ b/packages/geo/RasterSampler.cpp @@ -97,7 +97,7 @@ const RecordObject::fieldDef_t RasterSampler::fileIdRecDef[] = { ******************************************************************************/ /*---------------------------------------------------------------------------- - * luaCreate - :sampler(, , , , , ) + * luaCreate - :sampler(, , , , ) *----------------------------------------------------------------------------*/ int RasterSampler::luaCreate (lua_State* L) { From a7711e5638073cfc033eccaa2a95e8c637a95686 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Tue, 5 Mar 2024 20:32:12 +0000 Subject: [PATCH 37/43] fix for arrow implementation - working ancillary fields in parquet files --- packages/arrow/ArrowImpl.cpp | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/arrow/ArrowImpl.cpp b/packages/arrow/ArrowImpl.cpp index ff05a072f..cfd7db6ed 100644 --- a/packages/arrow/ArrowImpl.cpp +++ b/packages/arrow/ArrowImpl.cpp @@ -1237,16 +1237,15 @@ void ArrowImpl::processAncillaryFields (vector>& column AncillaryFields::field_array_t* field_array = reinterpret_cast(batch->anc_records[j]->getRecordData()); for(uint32_t k = 0; k < field_array->num_fields; k++) { - AncillaryFields::field_t& field = field_array->fields[k]; - const char* name = NULL; - /* Get Name */ - if(field.field_index < ancillary_fields.size()) name = ancillary_fields[field.field_index].c_str(); - else throw RunTimeException(CRITICAL, RTE_ERROR, "Invalid field index: %d", field.field_index); + const char* name = NULL; + uint8_t field_index = field_array->fields[k].field_index; + if(field_index < ancillary_fields.size()) name = ancillary_fields[field_index].c_str(); + else throw RunTimeException(CRITICAL, RTE_ERROR, "Invalid field index: %d", field_index); /* Add to Field Table */ - field_table[name].push_back(&field); - field_type_table.add(name, static_cast(field.data_type), false); + field_table[name].push_back(&(field_array->fields[k])); + field_type_table.add(name, static_cast(field_array->fields[k].data_type), false); } } } @@ -1289,7 +1288,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; double* val_ptr = AncillaryFields::getValueAsDouble(field->value); builder.UnsafeAppend((double)*val_ptr); } @@ -1303,7 +1302,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; float* val_ptr = AncillaryFields::getValueAsFloat(field->value); builder.UnsafeAppend((float)*val_ptr); } @@ -1317,7 +1316,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); builder.UnsafeAppend((int8_t)*val_ptr); } @@ -1331,7 +1330,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); builder.UnsafeAppend((int16_t)*val_ptr); } @@ -1345,7 +1344,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); builder.UnsafeAppend((int32_t)*val_ptr); } @@ -1359,7 +1358,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); builder.UnsafeAppend((int64_t)*val_ptr); } @@ -1373,7 +1372,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); builder.UnsafeAppend((uint8_t)*val_ptr); } @@ -1387,7 +1386,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); builder.UnsafeAppend((uint16_t)*val_ptr); } @@ -1401,7 +1400,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); builder.UnsafeAppend((uint32_t)*val_ptr); } @@ -1415,7 +1414,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); builder.UnsafeAppend((uint64_t)*val_ptr); } @@ -1429,7 +1428,7 @@ void ArrowImpl::processAncillaryFields (vector>& column (void)builder.Reserve(num_rows); for(int j = 0; j < num_rows; j++) { - AncillaryFields::field_t* field = field_vec[i]; + AncillaryFields::field_t* field = field_vec[j]; int64_t* val_ptr = AncillaryFields::getValueAsInteger(field->value); builder.UnsafeAppend((int64_t)*val_ptr); } From ce03f2daa45ecda086ee9f7999690fab6185eb96 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Tue, 5 Mar 2024 21:26:00 +0000 Subject: [PATCH 38/43] added atl08 ancillary pytest --- .../data/boreal_tiles_v004_model_ready.gpkg | Bin 0 -> 1265664 bytes clients/python/tests/test_parquet.py | 56 ++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 clients/python/tests/data/boreal_tiles_v004_model_ready.gpkg diff --git a/clients/python/tests/data/boreal_tiles_v004_model_ready.gpkg b/clients/python/tests/data/boreal_tiles_v004_model_ready.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..05ed592252dc18a91faf42b146c8a1288413b636 GIT binary patch literal 1265664 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

&M&Sv&}g? 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

E7PqfyZ>!vebQfMyx}1p_-L~hyq|0v>r Date: Wed, 6 Mar 2024 12:11:12 +0000 Subject: [PATCH 39/43] fixed memory leak with atl08 ancillary fields --- plugins/icesat2/plugin/Icesat2Parms.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/icesat2/plugin/Icesat2Parms.cpp b/plugins/icesat2/plugin/Icesat2Parms.cpp index 9410fdf0d..f307e712e 100644 --- a/plugins/icesat2/plugin/Icesat2Parms.cpp +++ b/plugins/icesat2/plugin/Icesat2Parms.cpp @@ -477,6 +477,7 @@ void Icesat2Parms::cleanup (void) const delete atl03_geo_fields; delete atl03_ph_fields; delete atl06_fields; + delete atl08_fields; } /*---------------------------------------------------------------------------- From 3de9602e077c009ea0b892c242484a5d782fc3cc Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Wed, 6 Mar 2024 12:11:32 +0000 Subject: [PATCH 40/43] developer environment yml file for python client --- clients/python/environment.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 clients/python/environment.yml diff --git a/clients/python/environment.yml b/clients/python/environment.yml new file mode 100644 index 000000000..1ff48ba13 --- /dev/null +++ b/clients/python/environment.yml @@ -0,0 +1,22 @@ +# conda env create -f environment.yml +name: sliderule +channels: + - conda-forge +dependencies: + - geopandas + - numpy + - pandas + - pip + - pyarrow + - pyproj + - pytables + - pytest + - python + - requests + - scikit-learn + - scipy + - setuptools_scm + - shapely + - tk + - xyzservices + - sliderule From c2ec163a13f6ce5bb24c06bcd20353c7b6902e04 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Wed, 6 Mar 2024 12:49:16 +0000 Subject: [PATCH 41/43] updated pytests for warnings --- clients/python/tests/test_ancillary.py | 10 ++++++++-- clients/python/tests/test_arcticdem.py | 10 +++++----- clients/python/tests/test_parquet.py | 12 ++++++------ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/clients/python/tests/test_ancillary.py b/clients/python/tests/test_ancillary.py index 3014124f0..1720d1ef5 100644 --- a/clients/python/tests/test_ancillary.py +++ b/clients/python/tests/test_ancillary.py @@ -71,7 +71,13 @@ def test_atl08_phoreal(self, init): gdf = icesat2.atl08(parms, "ATL03_20181017222812_02950102_006_02.h5") assert init assert len(gdf) == 819 - assert abs(gdf["h_dif_ref"].quantile(q=.50) - -0.9146728515625) < 0.000001 + + print(gdf["h_dif_ref"].quantile(q=.50)) + print(gdf["rgt_y"].quantile(q=.50)) + print(gdf["sigma_atlas_land%"].quantile(q=.50)) + print(gdf["cloud_flag_atm%"].quantile(q=.50)) + + assert abs(gdf["h_dif_ref"].quantile(q=.50) - -0.9443359375) < 0.000001 assert abs(gdf["rgt_y"].quantile(q=.50) - 295.0) < 0.000001 - assert abs(gdf["sigma_atlas_land%"].quantile(q=.50) - 0.2402431309223175) < 0.000001 + assert abs(gdf["sigma_atlas_land%"].quantile(q=.50) - 0.24470525979995728) < 0.000001 assert abs(gdf["cloud_flag_atm%"].quantile(q=.50) - 1.0) < 0.000001 diff --git a/clients/python/tests/test_arcticdem.py b/clients/python/tests/test_arcticdem.py index 77efbcaf9..ded594986 100644 --- a/clients/python/tests/test_arcticdem.py +++ b/clients/python/tests/test_arcticdem.py @@ -62,8 +62,8 @@ def test_nearestneighbour(self, init): assert init assert len(gdf) == 957 assert len(gdf.keys()) == 20 - assert gdf["rgt"][0] == 1160 - assert gdf["cycle"][0] == 2 + assert gdf["rgt"].iloc[0] == 1160 + assert gdf["cycle"].iloc[0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 assert gdf['segment_id'].describe()["max"] == 405902 assert abs(gdf["mosaic.value"].describe()["min"] - 600.4140625) < sigma @@ -83,14 +83,14 @@ def test_zonal_stats(self, init): assert init assert len(gdf) == 957 assert len(gdf.keys()) == 27 - assert gdf["rgt"][0] == 1160 - assert gdf["cycle"][0] == 2 + assert gdf["rgt"].iloc[0] == 1160 + assert gdf["cycle"].iloc[0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 assert gdf['segment_id'].describe()["max"] == 405902 assert abs(gdf["mosaic.value"].describe()["min"] - 600.4140625) < sigma assert gdf["mosaic.count"].describe()["max"] == 81 assert gdf["mosaic.stdev"].describe()["count"] == 957 - assert gdf["mosaic.time"][0] == vrtFileTime + assert gdf["mosaic.time"].iloc[0] == vrtFileTime @pytest.mark.network class TestStrips: diff --git a/clients/python/tests/test_parquet.py b/clients/python/tests/test_parquet.py index 9051f57b8..d6d8107b3 100644 --- a/clients/python/tests/test_parquet.py +++ b/clients/python/tests/test_parquet.py @@ -29,8 +29,8 @@ def test_atl06(self, init): assert init assert len(gdf) == 957 assert len(gdf.keys()) == 17 - assert gdf["rgt"][0] == 1160 - assert gdf["cycle"][0] == 2 + assert gdf["rgt"].iloc[0] == 1160 + assert gdf["cycle"].iloc[0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 assert gdf['segment_id'].describe()["max"] == 405902 @@ -50,8 +50,8 @@ def test_atl06_non_geo(self, init): assert init assert len(gdf) == 957 assert len(gdf.keys()) == 18 - assert gdf["rgt"][0] == 1160 - assert gdf["cycle"][0] == 2 + assert gdf["rgt"].iloc[0] == 1160 + assert gdf["cycle"].iloc[0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 assert gdf['segment_id'].describe()["max"] == 405902 @@ -71,8 +71,8 @@ def test_atl03(self, init): assert init assert len(gdf) == 190491 assert len(gdf.keys()) == 22 - assert gdf["rgt"][0] == 1160 - assert gdf["cycle"][0] == 2 + assert gdf["rgt"].iloc[0] == 1160 + assert gdf["cycle"].iloc[0] == 2 assert gdf['segment_id'].describe()["min"] == 405231 assert gdf['segment_id'].describe()["max"] == 405902 From 350815bde3e6792f60088ca82f64e404bb2d0157 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Wed, 6 Mar 2024 12:49:53 +0000 Subject: [PATCH 42/43] removed print statements from pytests --- clients/python/tests/test_ancillary.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/clients/python/tests/test_ancillary.py b/clients/python/tests/test_ancillary.py index 1720d1ef5..bc9421420 100644 --- a/clients/python/tests/test_ancillary.py +++ b/clients/python/tests/test_ancillary.py @@ -71,12 +71,6 @@ def test_atl08_phoreal(self, init): gdf = icesat2.atl08(parms, "ATL03_20181017222812_02950102_006_02.h5") assert init assert len(gdf) == 819 - - print(gdf["h_dif_ref"].quantile(q=.50)) - print(gdf["rgt_y"].quantile(q=.50)) - print(gdf["sigma_atlas_land%"].quantile(q=.50)) - print(gdf["cloud_flag_atm%"].quantile(q=.50)) - assert abs(gdf["h_dif_ref"].quantile(q=.50) - -0.9443359375) < 0.000001 assert abs(gdf["rgt_y"].quantile(q=.50) - 295.0) < 0.000001 assert abs(gdf["sigma_atlas_land%"].quantile(q=.50) - 0.24470525979995728) < 0.000001 From d996e4929bcaf0ce56ab19870c4803d706d5d9f3 Mon Sep 17 00:00:00 2001 From: JP Swinski Date: Wed, 6 Mar 2024 14:48:14 +0000 Subject: [PATCH 43/43] setting cluster name for staged files --- clients/python/tests/test_meritdem.py | 2 +- packages/arrow/ParquetBuilder.cpp | 5 +-- packages/core/Dictionary.h | 8 ++--- packages/core/LuaLibrarySys.cpp | 23 ++++++++++++++ packages/core/LuaLibrarySys.h | 1 + platforms/linux/OsApi.cpp | 31 ++++++++++++++----- platforms/linux/OsApi.h | 9 ++++-- scripts/apps/server.lua | 3 ++ targets/slideruleearth-aws/docker-compose.yml | 2 +- 9 files changed, 67 insertions(+), 17 deletions(-) diff --git a/clients/python/tests/test_meritdem.py b/clients/python/tests/test_meritdem.py index 8bb5721b4..7b0989bc6 100644 --- a/clients/python/tests/test_meritdem.py +++ b/clients/python/tests/test_meritdem.py @@ -13,5 +13,5 @@ def test_sample(self, init): gdf = raster.sample("merit-dem", [[-172, 51.7], [-172, 51.71], [-172, 51.72], [-172, 51.73], [-172, 51.74]]) sliderule.set_rqst_timeout(default_request_timeout) assert init - assert gdf["value"][0] == -99990000 + assert gdf["value"].iloc[0] == -99990000 assert len(gdf) == 5 diff --git a/packages/arrow/ParquetBuilder.cpp b/packages/arrow/ParquetBuilder.cpp index 6821ee57c..91b854fa9 100644 --- a/packages/arrow/ParquetBuilder.cpp +++ b/packages/arrow/ParquetBuilder.cpp @@ -258,8 +258,9 @@ ParquetBuilder::ParquetBuilder (lua_State* L, ArrowParms* _parms, Asset* asset = dynamic_cast(LuaObject::getLuaObjectByName(parms->asset_name, Asset::OBJECT_TYPE)); const char* path_prefix = StringLib::match(asset->getDriver(), "s3") ? "s3://" : ""; const char* path_suffix = parms->as_geo ? ".geoparquet" : ".parquet"; - FString path_name("/%s.%016lX", id, OsApi::time(OsApi::CPU_CLK)); - FString path_str("%s%s%s%s", path_prefix, asset->getPath(), path_name.c_str(), path_suffix); + FString path_name("%s.%016lX", OsApi::getCluster(), OsApi::time(OsApi::CPU_CLK)); + bool use_provided_path = ((parms->path != NULL) && (parms->path[0] != '\0')); + FString path_str("%s%s/%s%s", path_prefix, asset->getPath(), use_provided_path ? parms->path : path_name.c_str(), path_suffix); asset->releaseLuaObject(); /* Set Output Path */ diff --git a/packages/core/Dictionary.h b/packages/core/Dictionary.h index 0c7cf0f35..902c1fbd2 100644 --- a/packages/core/Dictionary.h +++ b/packages/core/Dictionary.h @@ -465,9 +465,9 @@ int Dictionary::getKeys (char*** keys) const { if(hashTable[i].chain != EMPTY_ENTRY) { - char* new_key = NULL; + const char* new_key = NULL; OsApi::dupstr(&new_key, hashTable[i].key); - (*keys)[j++] = new_key; + (*keys)[j++] = (char*)new_key; } } @@ -618,7 +618,7 @@ Dictionary& Dictionary::operator=(const Dictionary& other) hashTable[i].prev = other.hashTable[i].prev; /* copy key */ - char* new_key = NULL; + const char* new_key = NULL; OsApi::dupstr(&new_key, other.hashTable[i].key); hashTable[i].key = new_key; } @@ -726,7 +726,7 @@ void Dictionary::addNode (const char* key, const T& data, unsigned int hash, } else { - char* tmp_key = NULL; + const char* tmp_key = NULL; OsApi::dupstr(&tmp_key, key); new_key = tmp_key; } diff --git a/packages/core/LuaLibrarySys.cpp b/packages/core/LuaLibrarySys.cpp index 5a1d3de0f..95f9a040d 100644 --- a/packages/core/LuaLibrarySys.cpp +++ b/packages/core/LuaLibrarySys.cpp @@ -55,6 +55,7 @@ const struct luaL_Reg LuaLibrarySys::sysLibs [] = { {"lsmsgq", LuaLibrarySys::lsys_lsmsgq}, {"setenvver", LuaLibrarySys::lsys_setenvver}, {"setispublic", LuaLibrarySys::lsys_setispublic}, + {"setcluster", LuaLibrarySys::lsys_setcluster}, {"type", LuaLibrarySys::lsys_type}, {"setstddepth", LuaLibrarySys::lsys_setstddepth}, {"setiosz", LuaLibrarySys::lsys_setiosize}, @@ -323,6 +324,28 @@ int LuaLibrarySys::lsys_setispublic (lua_State* L) return 1; } +/*---------------------------------------------------------------------------- + * lsys_setcluster + *----------------------------------------------------------------------------*/ +int LuaLibrarySys::lsys_setcluster (lua_State* L) +{ + bool status = true; + const char* cluster_str = NULL; + if(lua_isstring(L, 1)) + { + cluster_str = lua_tostring(L, 1); + OsApi::setCluster(cluster_str); + } + else + { + mlog(CRITICAL, "Invalid parameter supplied to set cluster, must be a string"); + status = false; + } + + lua_pushboolean(L, status); + return 1; +} + /*---------------------------------------------------------------------------- * lsys_type - TODO - needs to handle userdata types *----------------------------------------------------------------------------*/ diff --git a/packages/core/LuaLibrarySys.h b/packages/core/LuaLibrarySys.h index 2fbc30ac5..ac3bb704d 100644 --- a/packages/core/LuaLibrarySys.h +++ b/packages/core/LuaLibrarySys.h @@ -80,6 +80,7 @@ class LuaLibrarySys static int lsys_lsmsgq (lua_State* L); static int lsys_setenvver (lua_State* L); static int lsys_setispublic (lua_State* L); + static int lsys_setcluster (lua_State* L); static int lsys_type (lua_State* L); static int lsys_setstddepth (lua_State* L); static int lsys_setiosize (lua_State* L); diff --git a/platforms/linux/OsApi.cpp b/platforms/linux/OsApi.cpp index da6a1b30a..cf1495710 100644 --- a/platforms/linux/OsApi.cpp +++ b/platforms/linux/OsApi.cpp @@ -61,8 +61,9 @@ OsApi::print_func_t OsApi::print_func = NULL; int OsApi::io_timeout = IO_DEFAULT_TIMEOUT; int OsApi::io_maxsize = IO_DEFAULT_MAXSIZE; int64_t OsApi::launch_time = 0; -char* OsApi::environment_version = NULL; +const char* OsApi::environment_version = "unknown"; bool OsApi::is_public = false; +const char* OsApi::cluster_name = "localhost"; /****************************************************************************** * PUBLIC METHODS @@ -75,7 +76,6 @@ void OsApi::init(print_func_t _print_func) { memfd = open("/proc/meminfo", O_RDONLY); launch_time = OsApi::time(OsApi::SYS_CLK); - OsApi::dupstr(&environment_version, "unknown"); print_func = _print_func; } @@ -103,15 +103,16 @@ void OsApi::sleep(double secs) /*---------------------------------------------------------------------------- * dupstr *----------------------------------------------------------------------------*/ -void OsApi::dupstr (char** dst, const char* src) +void OsApi::dupstr (const char** dst, const char* src) { assert(dst); - if(*dst) delete [] *dst; + assert(src); int len = 0; while(src[len] != '\0') len++; - *dst = new char[len + 1]; - for(int k = 0; k < len; k++) (*dst)[k] = src[k]; - (*dst)[len] = '\0'; + char* buf = new char[len + 1]; + for(int k = 0; k < len; k++) buf[k] = src[k]; + buf[len] = '\0'; + *dst = buf; } /*---------------------------------------------------------------------------- @@ -400,3 +401,19 @@ bool OsApi::getIsPublic (void) { return is_public; } + +/*---------------------------------------------------------------------------- + * setEnvVersion + *----------------------------------------------------------------------------*/ +void OsApi::setCluster (const char* cluster) +{ + OsApi::dupstr(&cluster_name, cluster); +} + +/*---------------------------------------------------------------------------- + * getEnvVersion + *----------------------------------------------------------------------------*/ +const char* OsApi::getCluster (void) +{ + return cluster_name; +} diff --git a/platforms/linux/OsApi.h b/platforms/linux/OsApi.h index 416a5ccf7..f9915761c 100644 --- a/platforms/linux/OsApi.h +++ b/platforms/linux/OsApi.h @@ -208,8 +208,9 @@ class OsApi static void init (print_func_t _print_func); static void deinit (void); + static void sleep (double secs); // sleep at highest resolution available on system - static void dupstr (char** dst, const char* src); + static void dupstr (const char** dst, const char* src); static int64_t time (int clkid); static int64_t timeres (int clkid); // returns the resolution of the clock static uint16_t swaps (uint16_t val); @@ -220,6 +221,7 @@ class OsApi static int nproc (void); static double memusage (void); static void print (const char* file_name, unsigned int line_number, const char* format_string, ...) __attribute__((format(printf, 3, 4))); + static bool setIOMaxsize (int maxsize); static int getIOMaxsize (void); static void setIOTimeout (int timeout); @@ -230,6 +232,8 @@ class OsApi static const char* getEnvVersion (void); static void setIsPublic (bool _is_public); static bool getIsPublic (void); + static void setCluster (const char* cluster); + static const char* getCluster (void); private: @@ -238,8 +242,9 @@ class OsApi static int io_timeout; static int io_maxsize; static int64_t launch_time; - static char* environment_version; + static const char* environment_version; static bool is_public; + static const char* cluster_name; }; #endif /* __osapi__ */ diff --git a/scripts/apps/server.lua b/scripts/apps/server.lua index 1dba95e03..740ccf1d5 100644 --- a/scripts/apps/server.lua +++ b/scripts/apps/server.lua @@ -51,6 +51,9 @@ sys.setenvver(environment_version) -- Set Is Public -- sys.setispublic(is_public) +-- Set ECluster Name -- +sys.setcluster(org_name) + -- Configure System Message Queue Depth -- sys.setstddepth(msgq_depth) diff --git a/targets/slideruleearth-aws/docker-compose.yml b/targets/slideruleearth-aws/docker-compose.yml index 5c6bda9de..690a27b15 100644 --- a/targets/slideruleearth-aws/docker-compose.yml +++ b/targets/slideruleearth-aws/docker-compose.yml @@ -19,7 +19,7 @@ services: - IPV4=127.0.0.1 - ORCHESTRATOR=http://127.0.0.1:8050 - IS_PUBLIC=False - - CLUSTER=sliderule + - CLUSTER=localhost - PROVISIONING_SYSTEM=https://ps.localhost - CONTAINER_REGISTRY=742127912612.dkr.ecr.us-west-2.amazonaws.com - ENVIRONMENT_VERSION=$ENVIRONMENT_VERSION