shareddata 6.85.18__tar.gz → 6.86.0__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.
- {shareddata-6.85.18/src/shareddata.egg-info → shareddata-6.86.0}/PKG-INFO +7 -5
- {shareddata-6.85.18 → shareddata-6.86.0}/README.md +6 -4
- {shareddata-6.85.18 → shareddata-6.86.0}/setup.py +1 -1
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/apidocs.yml +141 -12
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/collections.py +111 -26
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/tables.py +30 -17
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/timeseries.py +26 -18
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Auth/workers.py +1 -1
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/CacheRedis.py +4 -2
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/ClientAPI.py +38 -15
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/LogHandlerAPI.py +14 -14
- shareddata-6.86.0/src/SharedData/IO/SaveTables.py +326 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/ServerSocket.py +2 -2
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/ServerWebSocket.py +2 -2
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/StreamsCache.py +4 -4
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/StreamsPersist.py +2 -2
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/SyncTable.py +1 -1
- shareddata-6.86.0/src/SharedData/Routines/RoutineCommand.py +90 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/Worker.py +1 -1
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/WorkerLib.py +318 -62
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/SharedData.py +126 -38
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/SharedNumpy.py +75 -15
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Table.py +44 -13
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TableDisk.py +28 -45
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TableIndex.py +6 -1
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TimeSeriesDisk.py +30 -1
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/auth.py +20 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/route_health.py +1 -1
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/standalone.py +1 -1
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/build_info.py +3 -1
- {shareddata-6.85.18 → shareddata-6.86.0/src/shareddata.egg-info}/PKG-INFO +7 -5
- shareddata-6.85.18/src/SharedData/IO/SaveTables.py +0 -144
- shareddata-6.85.18/src/SharedData/Routines/RoutineCommand.py +0 -36
- {shareddata-6.85.18 → shareddata-6.86.0}/LICENSE +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/MANIFEST.in +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/pyproject.toml +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/setup.cfg +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/ServerGunicorn.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/ServerWaitress.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/__init__.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/apidocs-dark.css +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/auth.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/constants.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/download_paths.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/download_slots.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/install/worker.ps1 +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/install/worker.sh +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/listcache.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/__init__.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/cache.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/data.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/downloads.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/metadata.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/operations.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/schedules.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/sqlite.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/system.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/web_routes.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/routes/workers.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/API/utils.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Auth/__init__.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Auth/bootstrap.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Auth/migrate.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Auth/store.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/CollectionMongoDB.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/DataAPI/__init__.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/DataAPI/catalog.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/DataAPI/config.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/DataAPI/errors.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/DataAPI/indexes.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/DataAPI/readers.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/DataAPI/resolve.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/DataAPI/service.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Database.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Defaults.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/AWSEC2.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/AWSS3.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/AutoDocstrings.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/ClientSocket.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/ClientWebSocket.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/LogReader.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/MongoDBClient.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/TunnelWebSocket.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/IO/__init__.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Listing.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/LockWaiters.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Logger.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/MCP/__init__.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/MCP/__main__.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/MCP/client.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/MCP/server.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Metadata.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/MultiProc.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/OpenFIGI.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Redaction.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/BatchJob.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/ContainerHealth.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/JobStore.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/RoutineOrigin.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/Schedule.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/ScheduleMirror.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/ScheduleStore.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/Scheduler.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/WorkerPool.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Routines/__init__.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/SQLite.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/StreamKafka.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Symbol.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TableIndexJitFunctions.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TableIndexJitGenerate.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TableIndexJitHash.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TableIndexJitLoc.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TableProtocol.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TableRead.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TableRewrite.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/TimeseriesContainer.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Users.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Utils.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/__init__.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/activity_logger.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/app.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/assets/google_logo.png +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/assets/kafka_keys.js +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/assets/logs.css +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/assets/sched.css +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/assets/styles.css +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/backends.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/config.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/containers.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/identity.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/kafka_view.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/mongo_view.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/proc.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/redis_view.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/route_registry.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/sched.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/schedule_editor.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/server.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/services.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/shell.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/account.js +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/custom.css +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/favicon.ico +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/favicon.png +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/logo.png +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/logo_96x96.png +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/shell.css +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/shell.js +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/standalone.css +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/static/standalone.js +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/suggest.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/account.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/base.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/claim.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/claim_done.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/dash.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/index.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/login.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/login_2fa.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/partials/navbar.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/partials/scripts.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/recovery_codes.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/shell.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/templates/standalone.html +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/twofactor.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/utils.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/Web/workerpool.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/__init__.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/fleet.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/preflight.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/private_files.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/release_backup.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/server.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/services.py +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/SharedData/sharedmutexwin.pyd +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/shareddata.egg-info/SOURCES.txt +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/shareddata.egg-info/dependency_links.txt +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/shareddata.egg-info/entry_points.txt +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/src/shareddata.egg-info/requires.txt +0 -0
- {shareddata-6.85.18 → shareddata-6.86.0}/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.
|
|
3
|
+
Version: 6.86.0
|
|
4
4
|
Summary: Memory Mapped / Shared Memory Database with S3 repository
|
|
5
5
|
Home-page: https://shareddata.net
|
|
6
6
|
Author: shareddata
|
|
@@ -100,7 +100,7 @@ SharedData provides a comprehensive set of features for high-performance financi
|
|
|
100
100
|
- **📋 Comprehensive Logging System** - Enterprise-grade logging with multiple destinations
|
|
101
101
|
- **🌐 Remote API Client** - REST API for remote data access and operations
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
**[Read the manual](docs/index.md)** — [Table](docs/core/table.md) and [Time series](docs/core/timeseries.md) are the core. The HTTP spec is [Swagger](docs/guide/swagger.md).
|
|
104
104
|
|
|
105
105
|
## ⚡ Quick Start
|
|
106
106
|
|
|
@@ -169,7 +169,7 @@ For power users and complex scenarios:
|
|
|
169
169
|
- **Production Deployment** - High availability and monitoring
|
|
170
170
|
- **Monitoring & Debugging** - Performance profiling and health checks
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
**[Logging templates](docs/patterns/logging.md)** — log at the scheduler boundary; do not log inside the row loop.
|
|
173
173
|
|
|
174
174
|
## 🧭 Data by Symbol
|
|
175
175
|
|
|
@@ -232,10 +232,12 @@ MIT: free to use, change and distribute, commercially too. See the [LICENSE](htt
|
|
|
232
232
|
|
|
233
233
|
## 📚 Documentation Index
|
|
234
234
|
|
|
235
|
-
- **[
|
|
235
|
+
- **[Manual](docs/index.md)** — current Read the Docs home
|
|
236
|
+
- **[Table](docs/core/table.md)** and **[Time series](docs/core/timeseries.md)**
|
|
237
|
+
- **[Swagger](docs/guide/swagger.md)** — `src/SharedData/API/apidocs.yml`, version 6.86.0
|
|
236
238
|
- **[⚡ Quick Start](https://shareddata.net/docs/QUICK_START.md)** - Get started with SharedData in minutes
|
|
237
239
|
- **[🔧 Configuration](https://shareddata.net/docs/CONFIGURATION.md)** - Complete configuration reference
|
|
238
|
-
- **[
|
|
240
|
+
- **[Logging templates](docs/patterns/logging.md)** — scheduler heartbeat vs a quiet data loop
|
|
239
241
|
- **[🔄 Development](https://shareddata.net/docs/DEVELOPMENT.md)** - Contributing and development guidelines
|
|
240
242
|
|
|
241
243
|
---
|
|
@@ -25,7 +25,7 @@ SharedData provides a comprehensive set of features for high-performance financi
|
|
|
25
25
|
- **📋 Comprehensive Logging System** - Enterprise-grade logging with multiple destinations
|
|
26
26
|
- **🌐 Remote API Client** - REST API for remote data access and operations
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
**[Read the manual](docs/index.md)** — [Table](docs/core/table.md) and [Time series](docs/core/timeseries.md) are the core. The HTTP spec is [Swagger](docs/guide/swagger.md).
|
|
29
29
|
|
|
30
30
|
## ⚡ Quick Start
|
|
31
31
|
|
|
@@ -94,7 +94,7 @@ For power users and complex scenarios:
|
|
|
94
94
|
- **Production Deployment** - High availability and monitoring
|
|
95
95
|
- **Monitoring & Debugging** - Performance profiling and health checks
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
**[Logging templates](docs/patterns/logging.md)** — log at the scheduler boundary; do not log inside the row loop.
|
|
98
98
|
|
|
99
99
|
## 🧭 Data by Symbol
|
|
100
100
|
|
|
@@ -157,10 +157,12 @@ MIT: free to use, change and distribute, commercially too. See the [LICENSE](htt
|
|
|
157
157
|
|
|
158
158
|
## 📚 Documentation Index
|
|
159
159
|
|
|
160
|
-
- **[
|
|
160
|
+
- **[Manual](docs/index.md)** — current Read the Docs home
|
|
161
|
+
- **[Table](docs/core/table.md)** and **[Time series](docs/core/timeseries.md)**
|
|
162
|
+
- **[Swagger](docs/guide/swagger.md)** — `src/SharedData/API/apidocs.yml`, version 6.86.0
|
|
161
163
|
- **[⚡ Quick Start](https://shareddata.net/docs/QUICK_START.md)** - Get started with SharedData in minutes
|
|
162
164
|
- **[🔧 Configuration](https://shareddata.net/docs/CONFIGURATION.md)** - Complete configuration reference
|
|
163
|
-
- **[
|
|
165
|
+
- **[Logging templates](docs/patterns/logging.md)** — scheduler heartbeat vs a quiet data loop
|
|
164
166
|
- **[🔄 Development](https://shareddata.net/docs/DEVELOPMENT.md)** - Contributing and development guidelines
|
|
165
167
|
|
|
166
168
|
---
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
openapi: 3.0.3
|
|
3
3
|
info:
|
|
4
4
|
title: SharedData API
|
|
5
|
-
version: 6.
|
|
5
|
+
version: 6.86.0
|
|
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.
|
|
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
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1334
|
+
oneOf:
|
|
1335
|
+
- type: array
|
|
1336
|
+
nullable: true
|
|
1337
|
+
items:
|
|
1338
|
+
type: object
|
|
1339
|
+
- type: object
|
|
1333
1340
|
responses:
|
|
1334
1341
|
'201':
|
|
1335
|
-
description:
|
|
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:
|
|
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:
|
|
1343
|
-
|
|
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`.
|
|
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'] =
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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."""
|
|
@@ -15,6 +15,7 @@ from flask import Blueprint, Response, jsonify, request
|
|
|
15
15
|
from SharedData.CollectionMongoDB import CollectionMongoDB
|
|
16
16
|
from SharedData.Database import DATABASE_PKEYS
|
|
17
17
|
from SharedData.Table import TableNotFound
|
|
18
|
+
from SharedData.SharedNumpy import IndexNotAvailable
|
|
18
19
|
from SharedData.TableRead import generation_read
|
|
19
20
|
from SharedData.Listing import ListingBudget, ListingTimeout, ListingTooLarge
|
|
20
21
|
|
|
@@ -40,6 +41,15 @@ TABLE_LIST_SECONDS = 5.0
|
|
|
40
41
|
TABLE_LIST_ENTRIES = 10000
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
def _require_filter_indexes(tbl, startdate, enddate, symbols, portfolios, tags):
|
|
45
|
+
"""Validate filters before an empty response, without traversing their indexes."""
|
|
46
|
+
for kind, requested in (('date', startdate is not None or enddate is not None),
|
|
47
|
+
('symbol', symbols is not None),
|
|
48
|
+
('portfolio', portfolios is not None), ('tag', tags is not None)):
|
|
49
|
+
if requested:
|
|
50
|
+
tbl.require_index_for(kind)
|
|
51
|
+
|
|
52
|
+
|
|
43
53
|
def init_tables_routes(shdata, authenticate_fn):
|
|
44
54
|
global tables_bp
|
|
45
55
|
tables_bp = Blueprint('tables', __name__)
|
|
@@ -125,6 +135,11 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
125
135
|
response.status_code = 404
|
|
126
136
|
response.headers['Error-Message'] = str(e).replace('\n', ' ')
|
|
127
137
|
return response
|
|
138
|
+
except IndexNotAvailable as e:
|
|
139
|
+
# An index filter (startdate/enddate/symbols/portfolios/tags) this table's key has no index for.
|
|
140
|
+
response = make_error_response(e, status=400, error_type='IndexNotAvailable')
|
|
141
|
+
response.headers['Error-Message'] = str(e).replace('\n', ' ')
|
|
142
|
+
return response
|
|
128
143
|
except Exception as e:
|
|
129
144
|
busy = table_busy_response(e)
|
|
130
145
|
if busy is not None:
|
|
@@ -179,6 +194,14 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
179
194
|
tablepath = apply_subfolder(tablename, tablesubfolder)
|
|
180
195
|
if last < 0:
|
|
181
196
|
return jsonify({'error': 'last is a number of rows'}), 400
|
|
197
|
+
try:
|
|
198
|
+
startdate = pd.Timestamp(startdate) if startdate is not None else None
|
|
199
|
+
enddate = pd.Timestamp(enddate) if enddate is not None else None
|
|
200
|
+
except (ValueError, TypeError):
|
|
201
|
+
return jsonify({'error': 'startdate and enddate are dates, as 2026-10-07'}), 400
|
|
202
|
+
if (startdate is not None and pd.isna(startdate)) or (enddate is not None and pd.isna(enddate)):
|
|
203
|
+
return jsonify({'error': 'startdate and enddate are dates, as 2026-10-07'}), 400
|
|
204
|
+
|
|
182
205
|
has_mtime = 'mtime' in (tbl.dtype.names or ())
|
|
183
206
|
if by_since:
|
|
184
207
|
try:
|
|
@@ -190,6 +213,7 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
190
213
|
since = since.tz_convert('UTC').tz_localize(None) if since.tzinfo is not None else since
|
|
191
214
|
# Nothing was written to the table since then: answered without reading a row.
|
|
192
215
|
if float(tbl.mtime) <= since.timestamp():
|
|
216
|
+
_require_filter_indexes(tbl, startdate, enddate, symbols, portfolios, tags)
|
|
193
217
|
return make_empty_response(204)
|
|
194
218
|
since = np.datetime64(since.value, 'ns')
|
|
195
219
|
plain = not (startdate or enddate or symbols or portfolios or tags or query)
|
|
@@ -197,15 +221,11 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
197
221
|
loc = None
|
|
198
222
|
|
|
199
223
|
if startdate is not None and enddate is not None:
|
|
200
|
-
startdate = pd.Timestamp(startdate)
|
|
201
|
-
enddate = pd.Timestamp(enddate)
|
|
202
224
|
loc = tbl.get_date_loc_gte_lte(startdate, enddate)
|
|
203
225
|
elif startdate is not None:
|
|
204
|
-
|
|
205
|
-
loc = tbl.get_date_loc_gte(startdate)
|
|
226
|
+
loc = tbl.get_date_loc_gte(startdate)
|
|
206
227
|
elif enddate is not None:
|
|
207
|
-
|
|
208
|
-
loc = tbl.get_date_loc_lte(enddate)
|
|
228
|
+
loc = tbl.get_date_loc_lte(enddate)
|
|
209
229
|
elif plain and by_since:
|
|
210
230
|
loc = np.flatnonzero(np.asarray(tbl[:]['mtime']) > since) # one pass over one column
|
|
211
231
|
since = None
|
|
@@ -215,8 +235,9 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
215
235
|
loc = np.arange(tbl.count)
|
|
216
236
|
|
|
217
237
|
if len(loc) == 0:
|
|
238
|
+
_require_filter_indexes(tbl, startdate, enddate, symbols, portfolios, tags)
|
|
218
239
|
return make_empty_response(204)
|
|
219
|
-
|
|
240
|
+
|
|
220
241
|
# filter data by symbols
|
|
221
242
|
if symbols is not None:
|
|
222
243
|
symbols = symbols.split(',')
|
|
@@ -470,12 +491,12 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
470
491
|
|
|
471
492
|
if names is None or formats is None:
|
|
472
493
|
tbl = shdata.table(database, period, source, tablepath,
|
|
473
|
-
names=names, formats=formats, size=size,
|
|
494
|
+
names=names, formats=formats, size=size, grow=size is not None,
|
|
474
495
|
overwrite=overwrite, user=user, value=value,
|
|
475
496
|
hasindex=hasindex, is_schemaless=is_schemaless)
|
|
476
497
|
else:
|
|
477
498
|
tbl = shdata.table(database, period, source, tablepath,
|
|
478
|
-
names=names, formats=formats, size=size,
|
|
499
|
+
names=names, formats=formats, size=size, grow=size is not None,
|
|
479
500
|
overwrite=overwrite, user=user, value=None,
|
|
480
501
|
hasindex=hasindex, is_schemaless=is_schemaless)
|
|
481
502
|
if value is not None:
|
|
@@ -484,14 +505,6 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
484
505
|
else:
|
|
485
506
|
tbl = tbl.extend(value)
|
|
486
507
|
|
|
487
|
-
if size is not None:
|
|
488
|
-
if tbl.size < size:
|
|
489
|
-
tbl.free()
|
|
490
|
-
tbl = shdata.table(database, period, source, tablepath,
|
|
491
|
-
names=names, formats=formats, size=size,
|
|
492
|
-
overwrite=overwrite, user=user,
|
|
493
|
-
hasindex=hasindex, is_schemaless=is_schemaless)
|
|
494
|
-
|
|
495
508
|
response = Response(status=201)
|
|
496
509
|
response.headers['Content-Length'] = 0
|
|
497
510
|
return response
|