shareddata 6.85.7__tar.gz → 6.85.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. {shareddata-6.85.7/src/shareddata.egg-info → shareddata-6.85.9}/PKG-INFO +1 -1
  2. {shareddata-6.85.7 → shareddata-6.85.9}/setup.py +1 -1
  3. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/cache.py +11 -1
  4. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/SaveTables.py +1 -1
  5. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/SharedData.py +5 -1
  6. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/SharedNumpy.py +2 -2
  7. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Table.py +269 -32
  8. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableRewrite.py +104 -6
  9. {shareddata-6.85.7 → shareddata-6.85.9/src/shareddata.egg-info}/PKG-INFO +1 -1
  10. {shareddata-6.85.7 → shareddata-6.85.9}/LICENSE +0 -0
  11. {shareddata-6.85.7 → shareddata-6.85.9}/MANIFEST.in +0 -0
  12. {shareddata-6.85.7 → shareddata-6.85.9}/README.md +0 -0
  13. {shareddata-6.85.7 → shareddata-6.85.9}/pyproject.toml +0 -0
  14. {shareddata-6.85.7 → shareddata-6.85.9}/setup.cfg +0 -0
  15. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/ServerGunicorn.py +0 -0
  16. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/ServerWaitress.py +0 -0
  17. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/__init__.py +0 -0
  18. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/apidocs-dark.css +0 -0
  19. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/apidocs.yml +0 -0
  20. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/auth.py +0 -0
  21. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/constants.py +0 -0
  22. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/download_paths.py +0 -0
  23. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/download_slots.py +0 -0
  24. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/install/worker.ps1 +0 -0
  25. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/install/worker.sh +0 -0
  26. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/listcache.py +0 -0
  27. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/__init__.py +0 -0
  28. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/collections.py +0 -0
  29. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/data.py +0 -0
  30. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/downloads.py +0 -0
  31. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/metadata.py +0 -0
  32. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/operations.py +0 -0
  33. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/schedules.py +0 -0
  34. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/sqlite.py +0 -0
  35. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/system.py +0 -0
  36. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/tables.py +0 -0
  37. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/timeseries.py +0 -0
  38. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/web_routes.py +0 -0
  39. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/routes/workers.py +0 -0
  40. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/API/utils.py +0 -0
  41. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Auth/__init__.py +0 -0
  42. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Auth/bootstrap.py +0 -0
  43. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Auth/migrate.py +0 -0
  44. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Auth/store.py +0 -0
  45. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Auth/workers.py +0 -0
  46. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/CacheRedis.py +0 -0
  47. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/CollectionMongoDB.py +0 -0
  48. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/__init__.py +0 -0
  49. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/catalog.py +0 -0
  50. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/config.py +0 -0
  51. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/errors.py +0 -0
  52. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/indexes.py +0 -0
  53. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/readers.py +0 -0
  54. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/resolve.py +0 -0
  55. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/DataAPI/service.py +0 -0
  56. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Database.py +0 -0
  57. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Defaults.py +0 -0
  58. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/AWSEC2.py +0 -0
  59. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/AWSS3.py +0 -0
  60. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/AutoDocstrings.py +0 -0
  61. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/ClientAPI.py +0 -0
  62. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/ClientSocket.py +0 -0
  63. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/ClientWebSocket.py +0 -0
  64. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/LogHandlerAPI.py +0 -0
  65. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/LogReader.py +0 -0
  66. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/MongoDBClient.py +0 -0
  67. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/ServerSocket.py +0 -0
  68. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/ServerWebSocket.py +0 -0
  69. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/StreamsCache.py +0 -0
  70. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/StreamsPersist.py +0 -0
  71. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/SyncTable.py +0 -0
  72. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/TunnelWebSocket.py +0 -0
  73. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/IO/__init__.py +0 -0
  74. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Listing.py +0 -0
  75. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/LockWaiters.py +0 -0
  76. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Logger.py +0 -0
  77. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/MCP/__init__.py +0 -0
  78. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/MCP/__main__.py +0 -0
  79. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/MCP/client.py +0 -0
  80. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/MCP/server.py +0 -0
  81. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Metadata.py +0 -0
  82. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/MultiProc.py +0 -0
  83. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/OpenFIGI.py +0 -0
  84. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Redaction.py +0 -0
  85. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
  86. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/BatchJob.py +0 -0
  87. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/ContainerHealth.py +0 -0
  88. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/JobStore.py +0 -0
  89. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/RoutineCommand.py +0 -0
  90. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/RoutineOrigin.py +0 -0
  91. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/Schedule.py +0 -0
  92. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/ScheduleMirror.py +0 -0
  93. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
  94. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/ScheduleStore.py +0 -0
  95. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/Scheduler.py +0 -0
  96. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
  97. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/Worker.py +0 -0
  98. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
  99. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/WorkerLib.py +0 -0
  100. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/WorkerPool.py +0 -0
  101. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Routines/__init__.py +0 -0
  102. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/SQLite.py +0 -0
  103. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/StreamKafka.py +0 -0
  104. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Symbol.py +0 -0
  105. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableDisk.py +0 -0
  106. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndex.py +0 -0
  107. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndexJitFunctions.py +0 -0
  108. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
  109. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndexJitGenerate.py +0 -0
  110. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndexJitHash.py +0 -0
  111. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableIndexJitLoc.py +0 -0
  112. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableProtocol.py +0 -0
  113. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TableRead.py +0 -0
  114. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TimeSeriesDisk.py +0 -0
  115. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/TimeseriesContainer.py +0 -0
  116. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Users.py +0 -0
  117. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Utils.py +0 -0
  118. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
  119. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/__init__.py +0 -0
  120. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/activity_logger.py +0 -0
  121. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/app.py +0 -0
  122. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
  123. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
  124. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/google_logo.png +0 -0
  125. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/kafka_keys.js +0 -0
  126. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/logs.css +0 -0
  127. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/sched.css +0 -0
  128. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/assets/styles.css +0 -0
  129. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/auth.py +0 -0
  130. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/backends.py +0 -0
  131. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/config.py +0 -0
  132. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/containers.py +0 -0
  133. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/identity.py +0 -0
  134. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/kafka_view.py +0 -0
  135. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/mongo_view.py +0 -0
  136. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/proc.py +0 -0
  137. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/redis_view.py +0 -0
  138. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/route_health.py +0 -0
  139. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/route_registry.py +0 -0
  140. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/sched.py +0 -0
  141. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/schedule_editor.py +0 -0
  142. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/server.py +0 -0
  143. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/services.py +0 -0
  144. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/shell.py +0 -0
  145. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/standalone.py +0 -0
  146. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/account.js +0 -0
  147. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/custom.css +0 -0
  148. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/favicon.ico +0 -0
  149. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/favicon.png +0 -0
  150. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/logo.png +0 -0
  151. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/logo_96x96.png +0 -0
  152. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
  153. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/shell.css +0 -0
  154. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/shell.js +0 -0
  155. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/standalone.css +0 -0
  156. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/static/standalone.js +0 -0
  157. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/suggest.py +0 -0
  158. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/account.html +0 -0
  159. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/base.html +0 -0
  160. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/claim.html +0 -0
  161. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/claim_done.html +0 -0
  162. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/dash.html +0 -0
  163. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/index.html +0 -0
  164. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/login.html +0 -0
  165. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/login_2fa.html +0 -0
  166. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/partials/navbar.html +0 -0
  167. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/partials/scripts.html +0 -0
  168. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/recovery_codes.html +0 -0
  169. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/shell.html +0 -0
  170. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/templates/standalone.html +0 -0
  171. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/twofactor.py +0 -0
  172. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/utils.py +0 -0
  173. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/Web/workerpool.py +0 -0
  174. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/__init__.py +0 -0
  175. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/build_info.py +0 -0
  176. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/fleet.py +0 -0
  177. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/preflight.py +0 -0
  178. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/private_files.py +0 -0
  179. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/release_backup.py +0 -0
  180. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/server.py +0 -0
  181. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/services.py +0 -0
  182. {shareddata-6.85.7 → shareddata-6.85.9}/src/SharedData/sharedmutexwin.pyd +0 -0
  183. {shareddata-6.85.7 → shareddata-6.85.9}/src/shareddata.egg-info/SOURCES.txt +0 -0
  184. {shareddata-6.85.7 → shareddata-6.85.9}/src/shareddata.egg-info/dependency_links.txt +0 -0
  185. {shareddata-6.85.7 → shareddata-6.85.9}/src/shareddata.egg-info/entry_points.txt +0 -0
  186. {shareddata-6.85.7 → shareddata-6.85.9}/src/shareddata.egg-info/requires.txt +0 -0
  187. {shareddata-6.85.7 → shareddata-6.85.9}/src/shareddata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shareddata
3
- Version: 6.85.7
3
+ Version: 6.85.9
4
4
  Summary: Memory Mapped / Shared Memory Database with S3 repository
5
5
  Home-page: https://shareddata.net
6
6
  Author: shareddata
@@ -66,7 +66,7 @@ test_requires = [
66
66
 
67
67
  setup(
68
68
  name='shareddata',
69
- version='6.85.7',
69
+ version='6.85.9',
70
70
  description='Memory Mapped / Shared Memory Database with S3 repository',
71
71
  author='shareddata',
72
72
  author_email='admin@shareddata.net',
@@ -8,6 +8,9 @@ import datetime
8
8
  import json
9
9
  import time
10
10
 
11
+ import numpy as np
12
+ from bson.objectid import ObjectId
13
+
11
14
  from flask import Blueprint, Response, jsonify, request
12
15
 
13
16
  from SharedData.Logger import Logger
@@ -20,10 +23,17 @@ from SharedData.API.utils import (
20
23
 
21
24
 
22
25
  class _DatetimeEncoder(json.JSONEncoder):
23
- """JSON encoder that serializes datetime/date objects as ISO 8601 strings."""
26
+ """JSON encoder for cached values: datetime/date as ISO 8601 strings, a Mongo ObjectId as its hex
27
+ string, NumPy scalars and arrays as plain numbers and lists."""
24
28
  def default(self, obj):
25
29
  if isinstance(obj, (datetime.datetime, datetime.date)):
26
30
  return obj.isoformat()
31
+ if isinstance(obj, ObjectId):
32
+ return str(obj)
33
+ if isinstance(obj, np.generic):
34
+ return obj.item()
35
+ if isinstance(obj, np.ndarray):
36
+ return obj.tolist()
27
37
  return super().default(obj)
28
38
 
29
39
 
@@ -55,7 +55,7 @@ def write_table(dftable_row: Dict[str, Any]) -> str:
55
55
  dftable_row['source'],
56
56
  tablename
57
57
  )
58
- tbl.write(force_write=True)
58
+ tbl.write(force_write=True, strict=True)
59
59
  return f"Success: {tablename}"
60
60
  except Exception as e:
61
61
  Logger.log.error(f"Error saving {dftable_row} {e}")
@@ -253,7 +253,11 @@ class SharedData:
253
253
  elif len(partitioningstr) == 8:
254
254
  partitioning = 'daily'
255
255
  else:
256
- if hasindex:
256
+ # A new indexed table of a period shorter than a day must be partitioned. Opening an existing
257
+ # one (no schema given) is not creating: legacy unpartitioned tables stay readable and savable.
258
+ creating = (names is not None or formats is not None or value is not None
259
+ or is_schemaless or size is not None)
260
+ if hasindex and creating:
257
261
  if period != 'D1':
258
262
  raise Exception(f'Please specify partitioning for {period} tables')
259
263
 
@@ -285,7 +285,7 @@ class SharedNumpy(np.ndarray):
285
285
  """
286
286
  self.table.trim()
287
287
 
288
- def write(self,force_write=False):
288
+ def write(self,force_write=False,strict=False):
289
289
  """
290
290
  Writes the table data to storage, optionally forcing the write operation.
291
291
 
@@ -300,7 +300,7 @@ class SharedNumpy(np.ndarray):
300
300
  if (self.table.type != 1 or sys.platform == 'linux') and (
301
301
  self.table.hdr['hasindex'] == 1 and self.table.hdr['isidxsorted'] == 0):
302
302
  self.sort_index()
303
- self.table.write(force_write)
303
+ self.table.write(force_write, strict)
304
304
 
305
305
  def rememmap(self):
306
306
  """
@@ -22,6 +22,7 @@ import uuid
22
22
 
23
23
  from SharedData.Logger import Logger
24
24
  from SharedData.LockWaiters import TableBusy
25
+ from SharedData.TableRewrite import BsonIntegrityError
25
26
  from SharedData.IO.AWSS3 import S3Upload, S3Download, S3ListFolder, S3GetMtime, S3DeleteFile, UpdateModTime
26
27
  from SharedData.TableIndex import TableIndex
27
28
  from SharedData.SharedNumpy import SharedNumpy
@@ -34,6 +35,17 @@ from SharedData.TableProtocol import (IDLE, WRITE, STRUCT, DIRTY, mode, epoch,
34
35
  writer_lock, take_thread_lock, structure, structural)
35
36
 
36
37
 
38
+ # A BSON side file is compacted before every upload that has something to return: a table with nothing
39
+ # orphaned (append-only) has nothing to defragment, and rewriting it would only copy identical bytes.
40
+ BSON_DEFRAG_MIN_BYTES = 64 * 1024
41
+ BSON_DEFRAG_MIN_FRACTION = 0.0
42
+ # The rewrite holds the table lock and copies every live byte: for a large BSON file that must buy
43
+ # something. Above BSON_DEFRAG_LARGE_BYTES of allocated BSON, compact once at least this fraction is orphaned
44
+ # (a 43 GB file with 23 MB orphaned is 0.05%: not worth minutes of blocked ingestion).
45
+ BSON_DEFRAG_LARGE_BYTES = 1024 * 1024 * 1024
46
+ BSON_DEFRAG_LARGE_MIN_FRACTION = 0.01
47
+
48
+
37
49
  class TableNotFound(FileNotFoundError):
38
50
  """The requested table is absent locally and remotely, with no creation schema."""
39
51
  def __init__(self, table):
@@ -1373,6 +1385,23 @@ class Table:
1373
1385
  return h1[:l] == h2[:l]
1374
1386
 
1375
1387
  ############### BSON MANAGEMENT ###############
1388
+ def _reset_bson_mapping(self):
1389
+ """Drop the BSON mapping and reopen the file by path: after a paired publication the old
1390
+ mapping belongs to an unlinked inode and its offsets no longer match the records."""
1391
+ if self.bson_mmap is not None:
1392
+ try:
1393
+ self.bson_mmap.close()
1394
+ except (BufferError, ValueError):
1395
+ pass
1396
+ self.bson_mmap = None
1397
+ if self.bson_file_handle is not None:
1398
+ self.bson_file_handle.close()
1399
+ self.bson_file_handle = None
1400
+ self.curr_bson_size = 0
1401
+ self.prev_bson_ptr = 0
1402
+ if self.is_schemaless and self.bsonpath.exists() and self.bsonpath.stat().st_size > 0:
1403
+ self.init_bson_mmap()
1404
+
1376
1405
  def init_bson_mmap(self, from_download=False):
1377
1406
  """Initialize or reopen memory-mapped BSON file with proper sizing.
1378
1407
 
@@ -1572,6 +1601,193 @@ class Table:
1572
1601
  data = bytes(self.bson_mmap[offset:offset + size])
1573
1602
  return bson.decode(data)
1574
1603
 
1604
+ def _bson_columns(self, count):
1605
+ """bson_ptr and bson_size of the committed rows as int64 (legacy tables declare them '|V8')."""
1606
+ pointers = self.records['bson_ptr'][:count]
1607
+ sizes = self.records['bson_size'][:count]
1608
+ if sizes.dtype.kind == 'V' and sizes.dtype.itemsize == 8:
1609
+ sizes = np.ascontiguousarray(sizes).view(np.int64)
1610
+ if pointers.dtype.kind == 'V' and pointers.dtype.itemsize == 8:
1611
+ pointers = np.ascontiguousarray(pointers).view(np.int64)
1612
+ return np.asarray(pointers, dtype=np.int64), np.asarray(sizes, dtype=np.int64)
1613
+
1614
+ def bson_usage(self):
1615
+ """What the BSON file holds: bytes referenced by live rows, the span allocated, the file size."""
1616
+ count = int(self.hdr['count'])
1617
+ file_size = self.bsonpath.stat().st_size if self.bsonpath.exists() else 0
1618
+ usage = dict(file_size=file_size, used=0, live=0, orphaned=0, live_rows=0, count=count)
1619
+ if count == 0:
1620
+ usage['used'] = usage['orphaned'] = int(self.hdr['next_bson_ptr'])
1621
+ return usage
1622
+ pointers, sizes = self._bson_columns(count)
1623
+ live = sizes > 0
1624
+ usage['live_rows'] = int(live.sum())
1625
+ usage['used'] = int(self.hdr['next_bson_ptr']) # a cursor past cleared documents is orphaned space
1626
+ if usage['live_rows']:
1627
+ usage['live'] = int(sizes[live].sum())
1628
+ usage['used'] = max(int((pointers[live] + sizes[live]).max()), usage['used'])
1629
+ usage['orphaned'] = max(usage['used'] - usage['live'], 0)
1630
+ return usage
1631
+
1632
+ def _bson_mismatches(self, pointers, sizes, live_idx):
1633
+ """Rows whose pointer does not lead to a BSON document of exactly the recorded size: the int32 at
1634
+ the pointer is the document length, and a document ends with a zero byte."""
1635
+ import mmap
1636
+ bad = []
1637
+ if not len(live_idx):
1638
+ return np.zeros(0, dtype=np.int64)
1639
+ with open(self.bsonpath, 'rb') as source:
1640
+ view = mmap.mmap(source.fileno(), 0, access=mmap.ACCESS_READ)
1641
+ data = None
1642
+ try:
1643
+ data = np.frombuffer(view, dtype=np.uint8)
1644
+ span = np.arange(4)
1645
+ for first in range(0, len(live_idx), 1 << 20):
1646
+ rows = live_idx[first:first + (1 << 20)]
1647
+ short = sizes[rows] < 5 # the smallest document, {}, is 5 bytes
1648
+ bad.append(rows[short])
1649
+ rows = rows[~short]
1650
+ start, length = pointers[rows], sizes[rows]
1651
+ head = data[start[:, None] + span].astype(np.int64)
1652
+ declared = head[:, 0] | (head[:, 1] << 8) | (head[:, 2] << 16) | (head[:, 3] << 24)
1653
+ ends_with_zero = data[start + length - 1] == 0
1654
+ bad.append(rows[(declared != length) | ~ends_with_zero])
1655
+ finally:
1656
+ data = None # release the buffer export before closing the map
1657
+ view.close()
1658
+ return np.concatenate(bad) if bad else np.zeros(0, dtype=np.int64)
1659
+
1660
+ def compact_bson_if_worthwhile(self):
1661
+ """Compact the BSON file when enough of it is orphaned; None when it is not worth a rewrite."""
1662
+ usage = self.bson_usage()
1663
+ if usage['orphaned'] < BSON_DEFRAG_MIN_BYTES \
1664
+ or usage['orphaned'] < BSON_DEFRAG_MIN_FRACTION * max(usage['used'], 1):
1665
+ return None
1666
+ if usage['used'] > BSON_DEFRAG_LARGE_BYTES \
1667
+ and usage['orphaned'] < BSON_DEFRAG_LARGE_MIN_FRACTION * usage['used']:
1668
+ return None
1669
+ return self.defragment_bson(acquire=False)
1670
+
1671
+ def _defragment_bson_disk(self, chunk_size, acquire):
1672
+ """Compact bson.bin of a disk table and move every bson_ptr, as ONE crash-safe publication.
1673
+
1674
+ The compacted BSON and the record image with the new pointers are built beside the live files
1675
+ (nothing live is written), then published as a pair by TableRewrite.publish_rewrite: BSON first,
1676
+ records second, with a version 2 descriptor that recovery rolls forward. Other processes reopen
1677
+ both files when the epoch published on leaving STRUCT reaches them (_refresh_mapping).
1678
+ """
1679
+ import mmap
1680
+ import hashlib
1681
+ from SharedData.TableRewrite import (RewriteError, copy_metadata, file_identity, fsync_directory,
1682
+ pending_path, publish_rewrite, require_rewrite_support, write_all)
1683
+ from SharedData.TableProtocol import structure
1684
+ require_rewrite_support()
1685
+ if not self.is_schemaless:
1686
+ raise Exception(f'Cannot defragment non-schemaless table {self.relpath}')
1687
+ if acquire:
1688
+ self.acquire()
1689
+ stage = None
1690
+ token = None
1691
+ try:
1692
+ self._sync_held_mapping()
1693
+ # The old BSON is the (old, old) recovery baseline: make its recent writes durable before any
1694
+ # replacement exists, or a power loss could keep durable old records over lost BSON bytes.
1695
+ if self.bson_mmap is not None:
1696
+ self.bson_mmap.flush()
1697
+ if self.bsonpath.exists():
1698
+ with open(self.bsonpath, 'rb') as baseline:
1699
+ os.fsync(baseline.fileno())
1700
+ count = int(self.hdr['count'])
1701
+ pointers, sizes = self._bson_columns(count) if count else (np.zeros(0, np.int64), np.zeros(0, np.int64))
1702
+ live_idx = np.flatnonzero(sizes > 0)
1703
+ if not self.bsonpath.exists():
1704
+ if len(live_idx):
1705
+ raise RewriteError(f'{self.relpath}: records reference a BSON file that does not exist')
1706
+ Logger.log.warning(f'No BSON file to defragment for {self.relpath}')
1707
+ return {'original_size': 0, 'new_size': 0, 'reclaimed': 0}
1708
+ original_size = self.bsonpath.stat().st_size
1709
+ if len(live_idx) and (np.any(pointers[live_idx] < 0) or np.any(pointers[live_idx] + sizes[live_idx] > original_size)):
1710
+ raise RewriteError(f'{self.relpath}: a record points outside its BSON file; nothing was changed')
1711
+ mismatched = self._bson_mismatches(pointers, sizes, live_idx)
1712
+ if len(mismatched):
1713
+ from SharedData.TableRewrite import BsonIntegrityError
1714
+ raise BsonIntegrityError(
1715
+ f'{self.relpath}: {len(mismatched)} of {len(live_idx)} records do not point at a BSON document '
1716
+ f'of their recorded size (first rows {mismatched[:5].tolist()}); not compacted, '
1717
+ 'the unreferenced bytes may be the documents those records lost')
1718
+ new_offsets = np.zeros(len(live_idx), dtype=np.int64)
1719
+ if len(live_idx):
1720
+ new_offsets[1:] = np.cumsum(sizes[live_idx])[:-1]
1721
+ payload = int(sizes[live_idx].sum()) if len(live_idx) else 0
1722
+ token = uuid.uuid4().hex
1723
+ stage = self.bsonpath.parent / ('bson.bin.stage.' + token)
1724
+ digest = hashlib.sha256()
1725
+ fd = os.open(stage, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600)
1726
+ try:
1727
+ copy_metadata(self.bsonpath, fd)
1728
+ if len(live_idx):
1729
+ with open(self.bsonpath, 'rb') as source:
1730
+ view = mmap.mmap(source.fileno(), 0, access=mmap.ACCESS_READ)
1731
+ try:
1732
+ message = f'Defragmenting BSON file: {self.relpath}'
1733
+ for first in tqdm(range(0, len(live_idx), chunk_size), desc=message, unit='chunk'):
1734
+ rows = live_idx[first:first + chunk_size]
1735
+ buffer = bytearray(int(sizes[rows].sum()))
1736
+ cursor = 0
1737
+ for pointer, size in zip(pointers[rows].tolist(), sizes[rows].tolist()):
1738
+ buffer[cursor:cursor + size] = view[pointer:pointer + size]
1739
+ cursor += size
1740
+ digest.update(buffer)
1741
+ write_all(fd, buffer)
1742
+ finally:
1743
+ view.close()
1744
+ if payload == 0: # keep the file mappable: a physical zero-length file is refused
1745
+ digest.update(b'\0')
1746
+ write_all(fd, b'\0')
1747
+ os.fsync(fd)
1748
+ finally:
1749
+ os.close(fd)
1750
+ fsync_directory(stage.parent)
1751
+ new_size = max(payload, 1)
1752
+ mapping = np.full(count, -1, dtype=np.int64)
1753
+ mapping[live_idx] = new_offsets
1754
+
1755
+ def move_pointers(chunk, first):
1756
+ moved = mapping[first:first + len(chunk)]
1757
+ selected = moved >= 0
1758
+ if selected.any():
1759
+ column = chunk['bson_ptr']
1760
+ if column.dtype.kind == 'V' and column.dtype.itemsize == 8:
1761
+ column = column.view(np.int64)
1762
+ column[selected] = moved[selected]
1763
+ return chunk
1764
+
1765
+ with structure(self):
1766
+ publish_rewrite(self, None, 'defrag', start=0, transform=move_pointers,
1767
+ header_updates={'next_bson_ptr': payload, 'mtime': max(datetime.now().timestamp(), float(self.hdr['mtime']) + 0.001)},
1768
+ bson=dict(token=token, stage=stage, sha256=digest.hexdigest(), size=new_size))
1769
+ stats = {'original_size': original_size, 'new_size': new_size,
1770
+ 'reclaimed': original_size - new_size, 'live_bytes': payload, 'live_rows': int(len(live_idx))}
1771
+ Logger.log.info(
1772
+ f'Defragmented {self.relpath} BSON file: {original_size/1024/1024:.2f}MB -> '
1773
+ f'{new_size/1024/1024:.2f}MB (reclaimed {stats["reclaimed"]/1024/1024:.2f}MB)')
1774
+ return stats
1775
+ except Exception as e:
1776
+ Logger.log.error(f'Failed to defragment BSON file for {self.relpath}: {e}')
1777
+ # With a pending descriptor the stages belong to recovery; without one nothing refers to them.
1778
+ if token is not None and not pending_path(self).exists():
1779
+ for leftover in (self.bsonpath.parent / ('bson.bin.stage.' + token),
1780
+ Path(str(self.filepath) + '.stage.' + token),
1781
+ Path(str(self.filepath) + '.rewrite.prepare.' + token)):
1782
+ try:
1783
+ leftover.unlink()
1784
+ except OSError:
1785
+ pass
1786
+ raise
1787
+ finally:
1788
+ if acquire:
1789
+ self.release()
1790
+
1575
1791
  def defragment_bson(self, chunk_size=100000, buffer_mb=512, acquire=True):
1576
1792
  """
1577
1793
  Defragments the BSON file IN-PLACE by creating a compacted copy, then copying it back to the original file.
@@ -1603,8 +1819,7 @@ class Table:
1603
1819
  Exception: If the table is not schemaless or if defragmentation fails.
1604
1820
  """
1605
1821
  if self.type == 1:
1606
- from SharedData.TableRewrite import RewriteError
1607
- raise RewriteError('disk BSON maintenance requires coordinated staged publication')
1822
+ return self._defragment_bson_disk(chunk_size, acquire)
1608
1823
  if not self.is_schemaless:
1609
1824
  raise Exception(f'Cannot defragment non-schemaless table {self.relpath}')
1610
1825
 
@@ -1795,7 +2010,7 @@ class Table:
1795
2010
  self.release()
1796
2011
 
1797
2012
  ############### WRITE ###############
1798
- def write(self, force_write=False):
2013
+ def write(self, force_write=False, strict=False):
1799
2014
  """
1800
2015
  Writes data to a file and uploads it concurrently using separate threads.
1801
2016
 
@@ -1811,19 +2026,47 @@ class Table:
1811
2026
  7. Handles exceptions by logging an error message and re-raising the exception.
1812
2027
  8. Releases the lock regardless of success or failure.
1813
2028
 
2029
+ A schemaless table whose BSON file is about to be uploaded is compacted first, in this thread
2030
+ and under this lock (the compaction is a STRUCT publication, so it cannot run in the upload
2031
+ thread), and before the header, checksums and timestamp are prepared, so that the uploaded head
2032
+ and tail carry the moved pointers.
2033
+
1814
2034
  Parameters:
1815
2035
  force_write (bool): If True, forces the header to be written even if not modified.
2036
+ strict (bool): If True an upload or flush failure raises. It is always logged with the table name.
1816
2037
 
1817
2038
  Raises:
1818
- Exception: If the write or upload operation fails.
2039
+ Exception: If the write fails (or, with strict, if the upload or the local flush fails).
1819
2040
  """
1820
2041
  errmsg = ''
1821
2042
  self.acquire()
1822
2043
  try:
1823
2044
 
1824
2045
  tini = time.time()
2046
+ from SharedData.services import s3_enabled
2047
+ remote = s3_enabled()
1825
2048
  # create header
1826
2049
  mtime = self.hdr['mtime']
2050
+ compacted = False
2051
+ if remote and self.is_schemaless and self.type == 1 and self.bsonpath.exists():
2052
+ remote_bson_mtime = S3GetMtime(str(self.bsonpath) + '.gzip', self.database_folder)
2053
+ if force_write or remote_bson_mtime is None or remote_bson_mtime < mtime:
2054
+ try:
2055
+ stats = self.compact_bson_if_worthwhile()
2056
+ except BsonIntegrityError as error:
2057
+ # Loud, every night, but the upload goes on as it was: nothing is rewritten.
2058
+ Logger.log.error('BSON compaction skipped: %s' % error)
2059
+ stats = None
2060
+ if stats is not None:
2061
+ compacted = True
2062
+ # New pointers: head, tail and BSON are all rewritten and all uploaded, under a
2063
+ # publication time later than anything the remote holds, so consumers see the change.
2064
+ newest = max([m for m in (remote_bson_mtime,
2065
+ S3GetMtime(str(self.headpath) + '.gzip', self.database_folder),
2066
+ S3GetMtime(str(self.tailpath) + '.gzip', self.database_folder))
2067
+ if m is not None] + [float(self.hdr['mtime'])])
2068
+ self.hdr['mtime'] = max(float(self.hdr['mtime']), newest + 0.001)
2069
+ mtime = self.hdr['mtime']
1827
2070
  write_head = False
1828
2071
  write_head = self.fill_header(force_write)
1829
2072
 
@@ -1833,21 +2076,32 @@ class Table:
1833
2076
  if self.bson_file_handle is not None:
1834
2077
  os.fsync(self.bson_file_handle.fileno())
1835
2078
 
1836
- from SharedData.services import s3_enabled
1837
- if not s3_enabled():
2079
+ if not remote:
1838
2080
  self.write_file()
1839
2081
  return
1840
2082
 
2083
+ failures = []
2084
+
2085
+ def guarded(label, target, *args):
2086
+ # A thread's exception never reaches join(): keep it, name the table, let the caller decide.
2087
+ try:
2088
+ target(*args)
2089
+ except BaseException as error:
2090
+ failures.append('%s: %s' % (label, error))
2091
+ Logger.log.error('%s %s failed: %s' % (self.relpath, label, error))
2092
+
1841
2093
  thread_s3 = threading.Thread(
1842
- target=self.upload, args=(write_head, mtime, force_write))
2094
+ target=guarded, args=('upload', self.upload, write_head, mtime, force_write or compacted, compacted))
1843
2095
  thread_s3.start()
1844
2096
 
1845
- thread_flush = threading.Thread(target=self.write_file)
2097
+ thread_flush = threading.Thread(target=guarded, args=('flush', self.write_file))
1846
2098
  thread_flush.start()
1847
2099
 
1848
2100
  # join threads
1849
2101
  thread_s3.join()
1850
2102
  thread_flush.join()
2103
+ if failures and strict:
2104
+ raise Exception('; '.join(failures))
1851
2105
 
1852
2106
  te = time.time() - tini
1853
2107
  datasize = self.hdr['count'] * self.hdr['itemsize'] / 1000000
@@ -2030,33 +2284,14 @@ class Table:
2030
2284
  return write_head
2031
2285
 
2032
2286
  ############### UPLOAD ###############
2033
- def upload(self, write_head, mtime, force_write=False):
2287
+ def upload(self, write_head, mtime, force_write=False, force_tail=False):
2034
2288
  """
2035
2289
  Uploads the head, tail, and BSON files to remote storage based on modification times and flags.
2036
2290
 
2037
- For schemaless tables, defragments the BSON file FIRST to ensure that the uploaded head/tail
2038
- contain the correct updated pointers for the defragmented BSON file.
2291
+ For schemaless tables the BSON file is uploaded after head and tail. Compaction is not done here:
2292
+ it is a structural publication and runs in write(), before the header is prepared.
2039
2293
  """
2040
- # DEFRAGMENT BSON FIRST (before uploading head/tail) for schemaless tables
2041
- # This ensures the pointers in records are updated before we upload them
2042
- if self.is_schemaless and self.bsonpath.exists():
2043
- bson_gzip_path = str(self.bsonpath) + '.gzip'
2044
- remote_bson_mtime = S3GetMtime(bson_gzip_path, self.database_folder)
2045
- remote_bson_is_updated = False
2046
- if remote_bson_mtime is not None:
2047
- remote_bson_is_updated = remote_bson_mtime >= mtime
2048
-
2049
- # Defragment if BSON needs upload (this updates pointers in records)
2050
- if not remote_bson_is_updated or force_write:
2051
- # Pass acquire=False because we're already in write() context with lock held
2052
- stats = self.defragment_bson(acquire=False)
2053
-
2054
- # Mark header as modified to force head/tail upload with new pointers
2055
- write_head = True
2056
- self.hdr['mtimehead'] = mtime
2057
- self.hdr['mtimetail'] = mtime
2058
-
2059
- # Now upload head/tail with UPDATED pointers
2294
+ # head/tail carry the pointers of the BSON uploaded below (compacted by write() when it was worth it)
2060
2295
  remote_head_mtime = S3GetMtime(str(self.headpath)+'.gzip', self.database_folder)
2061
2296
  remote_head_is_updated = False
2062
2297
  if not remote_head_mtime is None:
@@ -2070,7 +2305,7 @@ class Table:
2070
2305
  remote_tail_is_updated = False
2071
2306
  if not remote_tail_mtime is None:
2072
2307
  remote_tail_is_updated = remote_tail_mtime >= self.hdr['mtimetail']
2073
- if not remote_tail_is_updated:
2308
+ if force_tail or not remote_tail_is_updated:
2074
2309
  self.upload_tail(mtime)
2075
2310
  else:
2076
2311
  self.delete_tail()
@@ -2741,6 +2976,8 @@ class Table:
2741
2976
  if (dirty or self._mapping_epoch != current
2742
2977
  or self.size != int(self.hdr['recordssize'])):
2743
2978
  self.attach()
2979
+ if getattr(self, 'is_schemaless', False):
2980
+ self._reset_bson_mapping() # the BSON file is published together with data.bin
2744
2981
  if self.hasindex and not dirty:
2745
2982
  self.index.rememmap()
2746
2983
  self._mapping_epoch = current