shareddata 6.85.18__tar.gz → 6.86.1__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 (192) hide show
  1. shareddata-6.86.1/PKG-INFO +164 -0
  2. shareddata-6.86.1/README.md +89 -0
  3. {shareddata-6.85.18 → shareddata-6.86.1}/setup.py +2 -2
  4. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/apidocs.yml +141 -12
  5. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/collections.py +111 -26
  6. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/tables.py +30 -17
  7. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/timeseries.py +26 -18
  8. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Auth/workers.py +1 -1
  9. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/CacheRedis.py +4 -2
  10. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/ClientAPI.py +38 -15
  11. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/LogHandlerAPI.py +14 -14
  12. shareddata-6.86.1/src/SharedData/IO/SaveTables.py +326 -0
  13. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/ServerSocket.py +2 -2
  14. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/ServerWebSocket.py +2 -2
  15. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/StreamsCache.py +4 -4
  16. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/StreamsPersist.py +2 -2
  17. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/SyncTable.py +1 -1
  18. shareddata-6.86.1/src/SharedData/Routines/RoutineCommand.py +90 -0
  19. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/Worker.py +1 -1
  20. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/WorkerLib.py +318 -62
  21. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/SharedData.py +126 -38
  22. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/SharedNumpy.py +75 -15
  23. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Table.py +44 -13
  24. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TableDisk.py +28 -45
  25. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TableIndex.py +6 -1
  26. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TimeSeriesDisk.py +30 -1
  27. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/auth.py +20 -0
  28. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/route_health.py +1 -1
  29. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/standalone.py +1 -1
  30. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/build_info.py +3 -1
  31. shareddata-6.86.1/src/shareddata.egg-info/PKG-INFO +164 -0
  32. shareddata-6.85.18/PKG-INFO +0 -243
  33. shareddata-6.85.18/README.md +0 -168
  34. shareddata-6.85.18/src/SharedData/IO/SaveTables.py +0 -144
  35. shareddata-6.85.18/src/SharedData/Routines/RoutineCommand.py +0 -36
  36. shareddata-6.85.18/src/shareddata.egg-info/PKG-INFO +0 -243
  37. {shareddata-6.85.18 → shareddata-6.86.1}/LICENSE +0 -0
  38. {shareddata-6.85.18 → shareddata-6.86.1}/MANIFEST.in +0 -0
  39. {shareddata-6.85.18 → shareddata-6.86.1}/pyproject.toml +0 -0
  40. {shareddata-6.85.18 → shareddata-6.86.1}/setup.cfg +0 -0
  41. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/ServerGunicorn.py +0 -0
  42. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/ServerWaitress.py +0 -0
  43. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/__init__.py +0 -0
  44. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/apidocs-dark.css +0 -0
  45. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/auth.py +0 -0
  46. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/constants.py +0 -0
  47. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/download_paths.py +0 -0
  48. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/download_slots.py +0 -0
  49. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/install/worker.ps1 +0 -0
  50. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/install/worker.sh +0 -0
  51. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/listcache.py +0 -0
  52. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/__init__.py +0 -0
  53. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/cache.py +0 -0
  54. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/data.py +0 -0
  55. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/downloads.py +0 -0
  56. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/metadata.py +0 -0
  57. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/operations.py +0 -0
  58. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/schedules.py +0 -0
  59. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/sqlite.py +0 -0
  60. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/system.py +0 -0
  61. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/web_routes.py +0 -0
  62. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/routes/workers.py +0 -0
  63. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/API/utils.py +0 -0
  64. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Auth/__init__.py +0 -0
  65. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Auth/bootstrap.py +0 -0
  66. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Auth/migrate.py +0 -0
  67. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Auth/store.py +0 -0
  68. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/CollectionMongoDB.py +0 -0
  69. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/DataAPI/__init__.py +0 -0
  70. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/DataAPI/catalog.py +0 -0
  71. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/DataAPI/config.py +0 -0
  72. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/DataAPI/errors.py +0 -0
  73. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/DataAPI/indexes.py +0 -0
  74. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/DataAPI/readers.py +0 -0
  75. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/DataAPI/resolve.py +0 -0
  76. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/DataAPI/service.py +0 -0
  77. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Database.py +0 -0
  78. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Defaults.py +0 -0
  79. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/AWSEC2.py +0 -0
  80. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/AWSS3.py +0 -0
  81. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/AutoDocstrings.py +0 -0
  82. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/ClientSocket.py +0 -0
  83. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/ClientWebSocket.py +0 -0
  84. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/LogReader.py +0 -0
  85. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/MongoDBClient.py +0 -0
  86. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/TunnelWebSocket.py +0 -0
  87. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/IO/__init__.py +0 -0
  88. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Listing.py +0 -0
  89. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/LockWaiters.py +0 -0
  90. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Logger.py +0 -0
  91. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/MCP/__init__.py +0 -0
  92. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/MCP/__main__.py +0 -0
  93. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/MCP/client.py +0 -0
  94. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/MCP/server.py +0 -0
  95. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Metadata.py +0 -0
  96. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/MultiProc.py +0 -0
  97. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/OpenFIGI.py +0 -0
  98. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Redaction.py +0 -0
  99. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
  100. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/BatchJob.py +0 -0
  101. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/ContainerHealth.py +0 -0
  102. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/JobStore.py +0 -0
  103. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/RoutineOrigin.py +0 -0
  104. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/Schedule.py +0 -0
  105. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/ScheduleMirror.py +0 -0
  106. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
  107. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/ScheduleStore.py +0 -0
  108. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/Scheduler.py +0 -0
  109. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
  110. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
  111. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/WorkerPool.py +0 -0
  112. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Routines/__init__.py +0 -0
  113. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/SQLite.py +0 -0
  114. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/StreamKafka.py +0 -0
  115. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Symbol.py +0 -0
  116. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TableIndexJitFunctions.py +0 -0
  117. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
  118. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TableIndexJitGenerate.py +0 -0
  119. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TableIndexJitHash.py +0 -0
  120. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TableIndexJitLoc.py +0 -0
  121. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TableProtocol.py +0 -0
  122. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TableRead.py +0 -0
  123. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TableRewrite.py +0 -0
  124. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/TimeseriesContainer.py +0 -0
  125. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Users.py +0 -0
  126. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Utils.py +0 -0
  127. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
  128. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/__init__.py +0 -0
  129. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/activity_logger.py +0 -0
  130. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/app.py +0 -0
  131. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
  132. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
  133. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/assets/google_logo.png +0 -0
  134. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/assets/kafka_keys.js +0 -0
  135. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/assets/logs.css +0 -0
  136. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/assets/sched.css +0 -0
  137. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/assets/styles.css +0 -0
  138. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/backends.py +0 -0
  139. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/config.py +0 -0
  140. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/containers.py +0 -0
  141. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/identity.py +0 -0
  142. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/kafka_view.py +0 -0
  143. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/mongo_view.py +0 -0
  144. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/proc.py +0 -0
  145. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/redis_view.py +0 -0
  146. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/route_registry.py +0 -0
  147. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/sched.py +0 -0
  148. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/schedule_editor.py +0 -0
  149. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/server.py +0 -0
  150. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/services.py +0 -0
  151. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/shell.py +0 -0
  152. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/account.js +0 -0
  153. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/custom.css +0 -0
  154. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/favicon.ico +0 -0
  155. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/favicon.png +0 -0
  156. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/logo.png +0 -0
  157. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/logo_96x96.png +0 -0
  158. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
  159. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/shell.css +0 -0
  160. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/shell.js +0 -0
  161. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/standalone.css +0 -0
  162. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/static/standalone.js +0 -0
  163. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/suggest.py +0 -0
  164. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/account.html +0 -0
  165. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/base.html +0 -0
  166. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/claim.html +0 -0
  167. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/claim_done.html +0 -0
  168. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/dash.html +0 -0
  169. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/index.html +0 -0
  170. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/login.html +0 -0
  171. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/login_2fa.html +0 -0
  172. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/partials/navbar.html +0 -0
  173. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/partials/scripts.html +0 -0
  174. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/recovery_codes.html +0 -0
  175. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/shell.html +0 -0
  176. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/templates/standalone.html +0 -0
  177. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/twofactor.py +0 -0
  178. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/utils.py +0 -0
  179. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/Web/workerpool.py +0 -0
  180. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/__init__.py +0 -0
  181. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/fleet.py +0 -0
  182. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/preflight.py +0 -0
  183. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/private_files.py +0 -0
  184. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/release_backup.py +0 -0
  185. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/server.py +0 -0
  186. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/services.py +0 -0
  187. {shareddata-6.85.18 → shareddata-6.86.1}/src/SharedData/sharedmutexwin.pyd +0 -0
  188. {shareddata-6.85.18 → shareddata-6.86.1}/src/shareddata.egg-info/SOURCES.txt +0 -0
  189. {shareddata-6.85.18 → shareddata-6.86.1}/src/shareddata.egg-info/dependency_links.txt +0 -0
  190. {shareddata-6.85.18 → shareddata-6.86.1}/src/shareddata.egg-info/entry_points.txt +0 -0
  191. {shareddata-6.85.18 → shareddata-6.86.1}/src/shareddata.egg-info/requires.txt +0 -0
  192. {shareddata-6.85.18 → shareddata-6.86.1}/src/shareddata.egg-info/top_level.txt +0 -0
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: shareddata
3
+ Version: 6.86.1
4
+ Summary: Memory-mapped record and time-series store with an HTTP API and a distributed scheduler
5
+ Home-page: https://shareddata.net
6
+ Author: shareddata
7
+ Author-email: admin@shareddata.net
8
+ License: MIT
9
+ Project-URL: Homepage, https://shareddata.net
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.11, <3.13
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: ipykernel<8,>=7.4
19
+ Requires-Dist: python-json-logger<5,>=4.2
20
+ Requires-Dist: python-dotenv<2,>=1.2.4
21
+ Requires-Dist: pandas<3,>=2.2
22
+ Requires-Dist: scipy<1.18,>=1.14
23
+ Requires-Dist: numpy<1.27,>=1.26.4
24
+ Requires-Dist: numba==0.59.1
25
+ Requires-Dist: XlsxWriter<4,>=3.2.9
26
+ Requires-Dist: openpyxl<4,>=3.1.5
27
+ Requires-Dist: tqdm<5,>=4.70.1
28
+ Requires-Dist: cffi<3,>=2.1
29
+ Requires-Dist: tzlocal<6,>=5.4.4
30
+ Requires-Dist: websockets<18,>=17.1
31
+ Requires-Dist: lz4<5,>=4.4.5
32
+ Requires-Dist: flask<4,>=3.1.3
33
+ Requires-Dist: waitress<4,>=3.0.2
34
+ Requires-Dist: requests<3,>=2.34.2
35
+ Requires-Dist: flasgger==0.9.7.1
36
+ Requires-Dist: pymongo<5,>=4.18.2
37
+ Requires-Dist: setuptools>=84
38
+ Requires-Dist: filelock<5,>=4.0.9
39
+ Requires-Dist: gunicorn<27,>=26.2
40
+ Requires-Dist: aiohttp<4,>=3.14.3
41
+ Requires-Dist: boto3<2,>=1.43
42
+ Requires-Dist: confluent-kafka<3,>=2.15.1
43
+ Requires-Dist: aiokafka<1,>=0.14
44
+ Requires-Dist: redis<9,>=8.1
45
+ Requires-Dist: Authlib<2,>=1.8
46
+ Requires-Dist: PyJWT<3,>=2.8
47
+ Requires-Dist: argon2-cffi<26,>=25.1
48
+ Requires-Dist: flask-jwt-extended<5,>=4.6
49
+ Requires-Dist: cryptography<51,>=50.0.2
50
+ Requires-Dist: segno<2,>=1.6
51
+ Requires-Dist: psutil<8,>=5.9
52
+ Requires-Dist: mcp>=1.27
53
+ Requires-Dist: httpx>=0.27
54
+ Provides-Extra: test
55
+ Requires-Dist: pytest<10,>=8.3.4; extra == "test"
56
+ Requires-Dist: pytest-asyncio<2,>=0.24; extra == "test"
57
+ Requires-Dist: playwright<2,>=1.63; extra == "test"
58
+ Provides-Extra: s3
59
+ Provides-Extra: kafka
60
+ Provides-Extra: redis
61
+ Provides-Extra: all
62
+ Dynamic: author
63
+ Dynamic: author-email
64
+ Dynamic: classifier
65
+ Dynamic: description
66
+ Dynamic: description-content-type
67
+ Dynamic: home-page
68
+ Dynamic: license
69
+ Dynamic: license-file
70
+ Dynamic: project-url
71
+ Dynamic: provides-extra
72
+ Dynamic: requires-dist
73
+ Dynamic: requires-python
74
+ Dynamic: summary
75
+
76
+ # SharedData
77
+
78
+ SharedData is an open-source memory-mapped store for records and time series —
79
+ in the workload category of tick stores such as kdb+ — plus the scheduler and
80
+ worker fleet that produce the data. Local processes map the same files through
81
+ the operating system's page cache; indexed writes coordinate through a
82
+ per-table process lock. Remote clients use an authenticated HTTP API; shared
83
+ memory does not span hosts.
84
+
85
+ Website and manual: <https://shareddata.net>
86
+
87
+ ## Install
88
+
89
+ Python 3.11 or 3.12:
90
+
91
+ ```sh
92
+ python -m pip install shareddata
93
+ ```
94
+
95
+ The [quick start](https://shareddata.net/docs/QUICK_START/) uses illustrative
96
+ records in isolated storage. See [configuration](https://shareddata.net/docs/CONFIGURATION/)
97
+ for public environment names and [Windows limitations](https://shareddata.net/docs/WINDOWS/)
98
+ for native requirements and rewrite behavior.
99
+
100
+ ## A record in one minute
101
+
102
+ Save as `example.py` after setting up isolated storage in the
103
+ [quick start](https://shareddata.net/docs/QUICK_START/), then run `python example.py`:
104
+
105
+ ```python
106
+ from SharedData.SharedData import SharedData
107
+ import pandas as pd
108
+
109
+ store = SharedData(__file__, user="example")
110
+ index = pd.MultiIndex.from_tuples(
111
+ [(pd.Timestamp("2026-01-02"), "EXAMPLE")],
112
+ names=["date", "symbol"],
113
+ )
114
+ rows = pd.DataFrame({"close": [100.0]}, index=index)
115
+ records = store.table(
116
+ "MarketData", "D1", "DEMO", "PRICES",
117
+ value=rows,
118
+ )
119
+ print(records.loc["2026-01-02", "EXAMPLE"])
120
+ ```
121
+
122
+ ## Containers and execution
123
+
124
+ - [Tables](https://shareddata.net/docs/core/table/): structured NumPy records and primary-key lookup.
125
+ - [Time series](https://shareddata.net/docs/core/timeseries/): timestamped float matrices.
126
+ - Collections, Redis caches, Kafka streams and optional S3 integration.
127
+ - [HTTP API](https://shareddata.net/docs/guide/swagger/): authenticated remote access.
128
+ - [Measured latency](https://shareddata.net/docs/performance/): what the containers cost in practice.
129
+
130
+ ## A store and the fleet that fills it
131
+
132
+ A routine is any program the Scheduler owns — a feed collector, a parser, a
133
+ consolidation job, an API server. A schedule lists the routines that should
134
+ stay alive or run at their times. Batch routines wait for their batch dependencies
135
+ to complete; realtime routines wait for their realtime dependencies to be running.
136
+ Dependencies between the two kinds are not enforced at runtime. Commands travel
137
+ through a per-host worker pool that validates the requested source revision
138
+ before every launch; routines report heartbeats and lifecycle messages through
139
+ the logging plane, and the Scheduler writes queryable state into the store.
140
+
141
+ - [Orchestration](https://shareddata.net/docs/orchestration/): the routine model, the Scheduler's tick and the worker pool.
142
+ - [Use cases](https://shareddata.net/docs/use-cases/): the workloads a production deployment runs, generalized.
143
+ - [Schedules](https://shareddata.net/docs/scheduler-store/) and [batch jobs](https://shareddata.net/docs/job-store/): where the state lives.
144
+
145
+ ## Data by symbol
146
+
147
+ The [semantic data API](https://shareddata.net/docs/data-api/) resolves a
148
+ symbol or entity to a dataset and includes provenance. It is a convenience,
149
+ not the fast path: producers keep using direct container interfaces for
150
+ performance-critical work.
151
+
152
+ ## Client integration
153
+
154
+ [Client integration](https://shareddata.net/docs/ecosystem/) covers
155
+ authenticated API access, the MCP server for compatible agent clients, and
156
+ worker access.
157
+
158
+ ## Development and license
159
+
160
+ Submit changes through the repository's review process. Read the
161
+ [development guide](https://shareddata.net/docs/DEVELOPMENT/) and run the
162
+ checks relevant to your change; [building and publishing](https://shareddata.net/docs/guide/building/)
163
+ explains how the public site is produced and audited. SharedData is released
164
+ under the MIT license; see `LICENSE` in the source distribution.
@@ -0,0 +1,89 @@
1
+ # SharedData
2
+
3
+ SharedData is an open-source memory-mapped store for records and time series —
4
+ in the workload category of tick stores such as kdb+ — plus the scheduler and
5
+ worker fleet that produce the data. Local processes map the same files through
6
+ the operating system's page cache; indexed writes coordinate through a
7
+ per-table process lock. Remote clients use an authenticated HTTP API; shared
8
+ memory does not span hosts.
9
+
10
+ Website and manual: <https://shareddata.net>
11
+
12
+ ## Install
13
+
14
+ Python 3.11 or 3.12:
15
+
16
+ ```sh
17
+ python -m pip install shareddata
18
+ ```
19
+
20
+ The [quick start](https://shareddata.net/docs/QUICK_START/) uses illustrative
21
+ records in isolated storage. See [configuration](https://shareddata.net/docs/CONFIGURATION/)
22
+ for public environment names and [Windows limitations](https://shareddata.net/docs/WINDOWS/)
23
+ for native requirements and rewrite behavior.
24
+
25
+ ## A record in one minute
26
+
27
+ Save as `example.py` after setting up isolated storage in the
28
+ [quick start](https://shareddata.net/docs/QUICK_START/), then run `python example.py`:
29
+
30
+ ```python
31
+ from SharedData.SharedData import SharedData
32
+ import pandas as pd
33
+
34
+ store = SharedData(__file__, user="example")
35
+ index = pd.MultiIndex.from_tuples(
36
+ [(pd.Timestamp("2026-01-02"), "EXAMPLE")],
37
+ names=["date", "symbol"],
38
+ )
39
+ rows = pd.DataFrame({"close": [100.0]}, index=index)
40
+ records = store.table(
41
+ "MarketData", "D1", "DEMO", "PRICES",
42
+ value=rows,
43
+ )
44
+ print(records.loc["2026-01-02", "EXAMPLE"])
45
+ ```
46
+
47
+ ## Containers and execution
48
+
49
+ - [Tables](https://shareddata.net/docs/core/table/): structured NumPy records and primary-key lookup.
50
+ - [Time series](https://shareddata.net/docs/core/timeseries/): timestamped float matrices.
51
+ - Collections, Redis caches, Kafka streams and optional S3 integration.
52
+ - [HTTP API](https://shareddata.net/docs/guide/swagger/): authenticated remote access.
53
+ - [Measured latency](https://shareddata.net/docs/performance/): what the containers cost in practice.
54
+
55
+ ## A store and the fleet that fills it
56
+
57
+ A routine is any program the Scheduler owns — a feed collector, a parser, a
58
+ consolidation job, an API server. A schedule lists the routines that should
59
+ stay alive or run at their times. Batch routines wait for their batch dependencies
60
+ to complete; realtime routines wait for their realtime dependencies to be running.
61
+ Dependencies between the two kinds are not enforced at runtime. Commands travel
62
+ through a per-host worker pool that validates the requested source revision
63
+ before every launch; routines report heartbeats and lifecycle messages through
64
+ the logging plane, and the Scheduler writes queryable state into the store.
65
+
66
+ - [Orchestration](https://shareddata.net/docs/orchestration/): the routine model, the Scheduler's tick and the worker pool.
67
+ - [Use cases](https://shareddata.net/docs/use-cases/): the workloads a production deployment runs, generalized.
68
+ - [Schedules](https://shareddata.net/docs/scheduler-store/) and [batch jobs](https://shareddata.net/docs/job-store/): where the state lives.
69
+
70
+ ## Data by symbol
71
+
72
+ The [semantic data API](https://shareddata.net/docs/data-api/) resolves a
73
+ symbol or entity to a dataset and includes provenance. It is a convenience,
74
+ not the fast path: producers keep using direct container interfaces for
75
+ performance-critical work.
76
+
77
+ ## Client integration
78
+
79
+ [Client integration](https://shareddata.net/docs/ecosystem/) covers
80
+ authenticated API access, the MCP server for compatible agent clients, and
81
+ worker access.
82
+
83
+ ## Development and license
84
+
85
+ Submit changes through the repository's review process. Read the
86
+ [development guide](https://shareddata.net/docs/DEVELOPMENT/) and run the
87
+ checks relevant to your change; [building and publishing](https://shareddata.net/docs/guide/building/)
88
+ explains how the public site is produced and audited. SharedData is released
89
+ under the MIT license; see `LICENSE` in the source distribution.
@@ -66,8 +66,8 @@ test_requires = [
66
66
 
67
67
  setup(
68
68
  name='shareddata',
69
- version='6.85.18',
70
- description='Memory Mapped / Shared Memory Database with S3 repository',
69
+ version='6.86.1',
70
+ description='Memory-mapped record and time-series store with an HTTP API and a distributed scheduler',
71
71
  author='shareddata',
72
72
  author_email='admin@shareddata.net',
73
73
  url='https://shareddata.net',
@@ -2,7 +2,7 @@
2
2
  openapi: 3.0.3
3
3
  info:
4
4
  title: SharedData API
5
- version: 6.85.1
5
+ version: 6.86.1
6
6
  description: |
7
7
  HTTP API of a SharedData server. The same operations are available from Python
8
8
  through `SharedData.IO.ClientAPI` (for example `ClientAPI.get_table(...)`).
@@ -811,6 +811,8 @@ paths:
811
811
  format: binary
812
812
  '204':
813
813
  description: No data found
814
+ '404':
815
+ description: JSON TagNotFound with type and message; the tag does not exist.
814
816
  '401':
815
817
  $ref: '#/components/responses/Unauthorized'
816
818
  '500':
@@ -854,7 +856,8 @@ paths:
854
856
  format: binary
855
857
  responses:
856
858
  '201':
857
- description: Data inserted
859
+ description: 'Data inserted. JSON with status, tag, message and `appended_columns`: the columns added
860
+ to an existing tag (empty for a new tag).'
858
861
  '401':
859
862
  $ref: '#/components/responses/Unauthorized'
860
863
  '500':
@@ -1111,7 +1114,8 @@ paths:
1111
1114
  '500':
1112
1115
  $ref: '#/components/responses/ServerError'
1113
1116
  description: 'Upserts rows sent as BSON+LZ4 by primary key; creates the table on first write when `names`,
1114
- `formats` and `size` are given. Python: `ClientAPI.post_table(...)`.'
1117
+ `formats` and `size` are given. A `size` larger than the capacity of an existing table grows it. Python:
1118
+ `ClientAPI.post_table(...)`.'
1115
1119
  delete:
1116
1120
  tags:
1117
1121
  - Tables
@@ -1290,7 +1294,7 @@ paths:
1290
1294
  post:
1291
1295
  tags:
1292
1296
  - Collections
1293
- summary: Insert documents (lz4+BSON or JSON)
1297
+ summary: Insert or upsert documents (lz4+BSON or JSON)
1294
1298
  parameters:
1295
1299
  - in: path
1296
1300
  name: database
@@ -1327,20 +1331,66 @@ paths:
1327
1331
  format: binary
1328
1332
  application/json:
1329
1333
  schema:
1330
- type: array
1331
- items:
1332
- type: object
1334
+ oneOf:
1335
+ - type: array
1336
+ nullable: true
1337
+ items:
1338
+ type: object
1339
+ - type: object
1333
1340
  responses:
1334
1341
  '201':
1335
- description: Created
1342
+ description: Batch accepted. An empty batch succeeds with documents equal to zero.
1343
+ content:
1344
+ application/json:
1345
+ schema:
1346
+ type: object
1347
+ required: [status, documents]
1348
+ properties:
1349
+ status:
1350
+ type: string
1351
+ enum: [success]
1352
+ documents:
1353
+ type: integer
1354
+ minimum: 0
1355
+ description: Documents accepted in this request (not the number of newly inserted rows).
1356
+ example: {status: success, documents: 0}
1336
1357
  '401':
1337
1358
  $ref: '#/components/responses/Unauthorized'
1338
1359
  '400':
1339
- description: Invalid data
1360
+ description: >-
1361
+ Empty or undecodable body, missing BSON data field, invalid document shape,
1362
+ missing primary-key fields, or an incompatible append/upsert mode.
1363
+ The entire request is rejected; none of its documents are written.
1364
+ headers:
1365
+ Error-Message:
1366
+ description: Reason the request was rejected, also returned in the JSON message.
1367
+ schema:
1368
+ type: string
1369
+ content:
1370
+ application/json:
1371
+ schema:
1372
+ type: object
1373
+ required: [type, message]
1374
+ properties:
1375
+ type:
1376
+ type: string
1377
+ enum: [BadRequest]
1378
+ message:
1379
+ type: string
1380
+ example: {type: BadRequest, message: "data must be a list of documents, not int"}
1340
1381
  '500':
1341
1382
  $ref: '#/components/responses/ServerError'
1342
- description: Inserts documents (lz4+BSON or JSON). With `hasindex` (default) documents are upserted by the
1343
- database's primary key; without it they are appended.
1383
+ description: >-
1384
+ Accepts LZ4-compressed BSON {"data": [documents]} with Content-Encoding lz4,
1385
+ or a plain JSON list. A single document object is also accepted. BSON data
1386
+ equal to null or [] (and JSON null or []) is an empty batch: returns 201 with
1387
+ documents equal to zero without opening or changing the collection.
1388
+ With hasindex=true (default), every document must contain every field of
1389
+ the database's primary key and documents are upserted; with hasindex=false,
1390
+ they are appended and the collection must have no primary-key index.
1391
+ Missing keys reject the whole request, including otherwise valid documents.
1392
+ A real storage/server failure returns 500 and can leave partial writes;
1393
+ only validation errors guarantee that nothing was written.
1344
1394
  patch:
1345
1395
  tags:
1346
1396
  - Collections
@@ -1835,7 +1885,9 @@ paths:
1835
1885
  '403':
1836
1886
  description: Master token required
1837
1887
  description: 'Each route: `id`, `command` (its name on the index), `path`, `target`, `group`, `description`,
1838
- `enabled`, `revision`. Master token.'
1888
+ `enabled`, `revision`. The body also has `activation`: `applied_generation`, `desired_generation`,
1889
+ and `state` (`unpublished` before any activation status has been recorded; later states include
1890
+ `pending`, `applied`, `unconfirmed`, and `failed`). Master token.'
1839
1891
  post:
1840
1892
  tags:
1841
1893
  - Routes
@@ -1880,6 +1932,21 @@ paths:
1880
1932
  description: 'Adds a service behind nginx on this deploy''s port: requests to `path` are proxied to `target`
1881
1933
  (a private or local http(s) URL), behind the SharedData sign-in. nginx is checked and reloaded before the
1882
1934
  answer, so the route works when it returns; `reachable` says whether the target answered. Master token.'
1935
+ /api/routes/activation/verify:
1936
+ post:
1937
+ tags:
1938
+ - Routes
1939
+ summary: Check that every configured proxy is serving this generation (master token)
1940
+ responses:
1941
+ '200':
1942
+ description: 'activation: applied_generation, desired_generation, state, and the probe results'
1943
+ '400':
1944
+ description: SHAREDDATA_WEB_PROXY_PROBES is missing or a probe URL is not http(s) without credentials
1945
+ '403':
1946
+ description: Master token required
1947
+ description: 'Reads `SHAREDDATA_WEB_PROXY_PROBES` (a JSON object of proxy name to marker URL) and GETs each
1948
+ URL with a 3 second timeout and no redirects. A probe passes when the status is 200 and the stripped body equals
1949
+ `{generation}:{sha256 of the catalog}`. Master token.'
1883
1950
  /api/routes/{route_id}:
1884
1951
  parameters:
1885
1952
  - name: route_id
@@ -2952,6 +3019,68 @@ paths:
2952
3019
  description: Unknown command token
2953
3020
  '403':
2954
3021
  description: Not a master
3022
+ /api/workers/shared-tokens:
3023
+ get:
3024
+ tags:
3025
+ - Workers
3026
+ summary: List shared worker tokens (master)
3027
+ description: One token shared by a group of interchangeable workers. The list is the label, the name pattern,
3028
+ the creator and the last use. The secret is not included. Tokens that workers already presented are listed
3029
+ after the ones created here.
3030
+ responses:
3031
+ '200':
3032
+ description: Shared worker tokens without their secret
3033
+ '403':
3034
+ description: Not a master
3035
+ post:
3036
+ tags:
3037
+ - Workers
3038
+ summary: Create a shared worker token (master)
3039
+ description: Shown once. `names` is the worker-name pattern, with `*` for the part that changes, for example
3040
+ `*@SCALE-*`. At least three characters must be fixed. The label is letters, digits, dot, dash or underscore,
3041
+ up to 40 characters. Revoke the existing label before creating it again.
3042
+ requestBody:
3043
+ required: true
3044
+ content:
3045
+ application/json:
3046
+ schema:
3047
+ type: object
3048
+ required:
3049
+ - label
3050
+ - names
3051
+ properties:
3052
+ label:
3053
+ type: string
3054
+ example: scale
3055
+ names:
3056
+ type: string
3057
+ example: '*@SCALE-*'
3058
+ responses:
3059
+ '201':
3060
+ description: '`{token, label, names, enabled, created_at, created_by, last_seen_at}` — `token` only on this response'
3061
+ '400':
3062
+ description: Invalid label or names, or a token with that label already exists
3063
+ '403':
3064
+ description: Not a master
3065
+ /api/workers/shared-tokens/{label}:
3066
+ delete:
3067
+ tags:
3068
+ - Workers
3069
+ summary: Revoke a shared worker token (master)
3070
+ parameters:
3071
+ - name: label
3072
+ in: path
3073
+ required: true
3074
+ schema:
3075
+ type: string
3076
+ description: The token stops working (API processes cache tokens up to 5 minutes).
3077
+ responses:
3078
+ '200':
3079
+ description: Revoked
3080
+ '404':
3081
+ description: Unknown shared worker token
3082
+ '403':
3083
+ description: Not a master
2955
3084
  /api/data/sources:
2956
3085
  get:
2957
3086
  tags:
@@ -6,6 +6,7 @@ import datetime
6
6
  import gzip
7
7
  import json
8
8
  import time
9
+ import traceback
9
10
 
10
11
  import bson
11
12
  from bson import json_util
@@ -17,6 +18,7 @@ from flask import Blueprint, Response, jsonify, request
17
18
 
18
19
  from SharedData.CollectionMongoDB import CollectionMongoDB
19
20
  from SharedData.Database import DATABASE_PKEYS
21
+ from SharedData.Logger import Logger, redact_credentials
20
22
 
21
23
  from SharedData.API.constants import (
22
24
  DEFAULT_USER,
@@ -36,6 +38,73 @@ from SharedData.API.utils import (
36
38
  # Blueprint definition
37
39
  collections_bp = Blueprint('collections', __name__)
38
40
 
41
+
42
+ class InvalidPayload(ValueError):
43
+ """The posted body is not something that can be written: answered 400 with this message."""
44
+
45
+
46
+ def payload_error(error, status=400):
47
+ """A 4xx whose reason is in the Error-Message header (what ClientAPI reports) and the JSON body."""
48
+ message = redact_credentials(error).replace('\r', ' ').replace('\n', ' ')
49
+ response = make_error_response(message, status=status, error_type='BadRequest')
50
+ response.headers['Error-Message'] = message
51
+ return response
52
+
53
+
54
+ def decode_posted_documents(request):
55
+ """The documents of a collection POST, as a list of dicts.
56
+
57
+ None (what ClientAPI sent for an empty list before 6.86, issue #112) is an empty batch; a single
58
+ document is a batch of one. Anything else that is not a list of documents raises InvalidPayload."""
59
+ if not request.data:
60
+ raise InvalidPayload('empty request body: send {"data": [documents]} (lz4 BSON) or a JSON list')
61
+ lz4 = request.headers.get('Content-Encoding', '').lower() == 'lz4'
62
+ try:
63
+ if lz4:
64
+ payload = bson.decode(lz4f.decompress(request.data))
65
+ else:
66
+ payload = json.loads(request.data)
67
+ except Exception as e:
68
+ raise InvalidPayload(f'request body could not be decoded as {"lz4 BSON" if lz4 else "JSON"}: '
69
+ f'{type(e).__name__}: {e}')
70
+ if lz4:
71
+ if 'data' not in payload:
72
+ raise InvalidPayload("lz4 BSON body has no 'data' field")
73
+ documents = payload['data']
74
+ else:
75
+ documents = payload
76
+ if documents is None:
77
+ return []
78
+ if isinstance(documents, dict):
79
+ return [documents]
80
+ if not isinstance(documents, list):
81
+ raise InvalidPayload(f'data must be a list of documents, not {type(documents).__name__}')
82
+ for i, document in enumerate(documents):
83
+ if not isinstance(document, dict):
84
+ raise InvalidPayload(f'document {i} is {type(document).__name__}, not an object '
85
+ f'(data must be a list of documents)')
86
+ return documents
87
+
88
+
89
+ def missing_pkey_report(documents, pkey_columns):
90
+ """Why these documents cannot be upserted, or '' when every one has every primary-key field."""
91
+ missing = {}
92
+ incomplete = 0
93
+ first = None
94
+ for i, document in enumerate(documents):
95
+ absent = [field for field in pkey_columns if field not in document]
96
+ if absent:
97
+ incomplete += 1
98
+ first = i if first is None else first
99
+ for field in absent:
100
+ missing[field] = missing.get(field, 0) + 1
101
+ if not incomplete:
102
+ return ''
103
+ fields = ', '.join(f'{field} ({count})' for field, count in missing.items())
104
+ return (f'{incomplete}/{len(documents)} documents miss primary-key fields: {fields}; '
105
+ f'the primary key is {", ".join(pkey_columns)} (first incomplete document: {first}); '
106
+ 'nothing was written')
107
+
39
108
  def parse_json_query(query: dict) -> dict:
40
109
  """
41
110
  Recursively parses a MongoDB-like query dictionary, converting string representations
@@ -145,11 +214,15 @@ def init_collections_routes(shdata, authenticate_fn):
145
214
  return jsonify({'error': 'method not allowed'}), 405
146
215
 
147
216
  except Exception as e:
217
+ # A server-side failure: keep its stack in the server log, the client only gets the message.
218
+ Logger.log.error(redact_credentials(f'collection {request.method} {database}/{period}/{source}/{tablename}'
219
+ f'{"/" + request.args["tablesubfolder"] if request.args.get("tablesubfolder") else ""}: '
220
+ f'{type(e).__name__}: {e}\n{traceback.format_exc()}'))
148
221
  time.sleep(ERROR_SLEEP_SECONDS)
149
- response = Response(status=500)
150
- response.headers['Error-Message'] = str(e).replace('\n', ' ')
222
+ response = Response(status=500)
223
+ response.headers['Error-Message'] = redact_credentials(e).replace('\r', ' ').replace('\n', ' ')
151
224
  return response
152
-
225
+
153
226
  def head_collection(database: str, period: str, source: str, tablename: str, request):
154
227
  """Handle HTTP HEAD requests by returning metadata headers for a specified MongoDB collection."""
155
228
  user = request.args.get('user', DEFAULT_USER)
@@ -302,37 +375,49 @@ def init_collections_routes(shdata, authenticate_fn):
302
375
  response.headers['Content-Length'] = len(response_json)
303
376
  return with_cursor(response)
304
377
 
305
- def post_collection(database, period, source, tablename, request):
306
- """Handle a POST request to insert or upsert a collection of documents."""
378
+ def post_collection(database, period, source, tablename, request):
379
+ """Handle a POST request to insert or upsert a collection of documents.
380
+
381
+ Body: lz4-compressed BSON {"data": [documents]} (what ClientAPI sends) or a plain JSON list.
382
+ A payload that is not a list of documents, or (upsert) documents missing a primary-key
383
+ field, is a 400 saying why and nothing is written. An empty batch is nothing to write."""
307
384
  tablesubfolder = request.args.get('tablesubfolder')
308
385
  user = request.args.get('user', DEFAULT_USER)
309
386
  hasindex = parse_bool_param(request.args.get('hasindex'), True)
310
387
 
311
388
  tablepath = apply_subfolder(tablename, tablesubfolder)
312
389
 
313
- collection = shdata.collection(database, period, source, tablepath, user=user, hasindex=hasindex)
314
-
315
- if not request.data:
316
- Response({'message': 'No data'}, status=400)
317
-
318
- if request.headers.get('Content-Encoding', '').lower() == 'lz4':
319
- decompressed = lz4f.decompress(request.data)
320
- documents = bson.decode(decompressed)['data']
321
- else:
322
- documents = json.loads(request.data)
323
-
324
- if not isinstance(documents, list):
325
- Response({'message': 'Data must be a list'}, status=400)
326
-
327
- if hasindex:
328
- collection.upsert(documents)
329
- else:
330
- collection.extend(documents)
331
-
332
- response_data = json.dumps({'status': 'success'}).encode('utf-8')
390
+ try:
391
+ documents = decode_posted_documents(request)
392
+ except InvalidPayload as e:
393
+ return payload_error(e)
394
+
395
+ if documents:
396
+ # Opening a collection can create it or upgrade its indexes. An empty upload
397
+ # must not do either (nor fail building a unique index on append-only data).
398
+ collection = shdata.collection(database, period, source, tablepath, user=user, hasindex=hasindex)
399
+ if hasindex:
400
+ if not collection.hasindex:
401
+ return payload_error(
402
+ f'{collection.relpath} has no unique index on its primary key '
403
+ f'({", ".join(collection.pkey_columns)}), so it cannot be upserted; '
404
+ 'post with hasindex=False to append')
405
+ missing = missing_pkey_report(documents, collection.pkey_columns)
406
+ if missing:
407
+ return payload_error(missing)
408
+ collection.upsert(documents)
409
+ else:
410
+ if collection.hasindex:
411
+ return payload_error(
412
+ f'{collection.relpath} has a unique index on its primary key '
413
+ f'({", ".join(collection.pkey_columns)}), so documents cannot be appended; '
414
+ 'post with hasindex=True to upsert')
415
+ collection.extend(documents)
416
+
417
+ response_data = json.dumps({'status': 'success', 'documents': len(documents)}).encode('utf-8')
333
418
  response = Response(response_data, status=201, mimetype='application/json')
334
419
  response.headers['Content-Length'] = str(len(response_data))
335
- return response
420
+ return response
336
421
 
337
422
  def patch_collection(database, period, source, tablename, request):
338
423
  """Update a single document in a specified collection within the database."""