shareddata 6.86.1__tar.gz → 6.94.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.
Potentially problematic release.
This version of shareddata might be problematic. Click here for more details.
- {shareddata-6.86.1/src/shareddata.egg-info → shareddata-6.94.0}/PKG-INFO +1 -1
- {shareddata-6.86.1 → shareddata-6.94.0}/setup.py +3 -1
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/__init__.py +8 -1
- shareddata-6.94.0/src/SharedData/API/access_log.py +306 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/auth.py +10 -2
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/data.py +1 -1
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/tables.py +24 -16
- shareddata-6.94.0/src/SharedData/Analytics/__init__.py +1 -0
- shareddata-6.94.0/src/SharedData/Analytics/cdie.py +283 -0
- shareddata-6.94.0/src/SharedData/Analytics/cdie_calendar.py +28 -0
- shareddata-6.94.0/src/SharedData/Analytics/cdie_store.py +74 -0
- shareddata-6.94.0/src/SharedData/Analytics/data/cdie/anbima_holidays.csv +235 -0
- shareddata-6.94.0/src/SharedData/Analytics/data/cdie/b3_session_overrides.csv +37 -0
- shareddata-6.94.0/src/SharedData/Analytics/data/cdie/copom.csv +33 -0
- shareddata-6.94.0/src/SharedData/Analytics/data/cdie/manifest.json +56 -0
- shareddata-6.94.0/src/SharedData/ConsensusGraph.py +869 -0
- shareddata-6.94.0/src/SharedData/DataAPI/cdie.py +72 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/DataAPI/service.py +6 -1
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/ClientAPI.py +94 -51
- shareddata-6.94.0/src/SharedData/KnowledgeBase.py +1474 -0
- shareddata-6.94.0/src/SharedData/KnowledgeContract.py +978 -0
- shareddata-6.94.0/src/SharedData/KnowledgeEvaluation.py +232 -0
- shareddata-6.94.0/src/SharedData/KnowledgeExperiment.py +335 -0
- shareddata-6.94.0/src/SharedData/KnowledgeGraph.py +536 -0
- shareddata-6.94.0/src/SharedData/KnowledgeLookup.py +156 -0
- shareddata-6.94.0/src/SharedData/KnowledgeMarket.py +417 -0
- shareddata-6.94.0/src/SharedData/KnowledgeOntology.py +176 -0
- shareddata-6.94.0/src/SharedData/KnowledgeResearch.py +481 -0
- shareddata-6.94.0/src/SharedData/KnowledgeScenario.py +200 -0
- shareddata-6.94.0/src/SharedData/KnowledgeXBRL.py +273 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Logger.py +3 -0
- shareddata-6.94.0/src/SharedData/ResearchMCP.py +432 -0
- shareddata-6.94.0/src/SharedData/Routines/PackageRoutine.py +174 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/RoutineCommand.py +8 -1
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/RoutineOrigin.py +6 -1
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/WorkerLib.py +28 -3
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/SharedData.py +21 -4
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/SharedNumpy.py +124 -8
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Table.py +120 -27
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TableDisk.py +5 -3
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TableIndex.py +96 -38
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TableIndexJitFunctions.py +1247 -48
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TableIndexJitGenerate.py +34 -14
- shareddata-6.94.0/src/SharedData/TableVector.py +140 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Utils.py +4 -2
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/schedule_editor.py +9 -2
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/workerpool.py +6 -1
- {shareddata-6.86.1 → shareddata-6.94.0/src/shareddata.egg-info}/PKG-INFO +1 -1
- {shareddata-6.86.1 → shareddata-6.94.0}/src/shareddata.egg-info/SOURCES.txt +25 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/shareddata.egg-info/entry_points.txt +1 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/LICENSE +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/MANIFEST.in +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/README.md +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/pyproject.toml +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/setup.cfg +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/ServerGunicorn.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/ServerWaitress.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/apidocs-dark.css +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/apidocs.yml +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/constants.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/download_paths.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/download_slots.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/install/worker.ps1 +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/install/worker.sh +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/listcache.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/__init__.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/cache.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/collections.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/downloads.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/metadata.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/operations.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/schedules.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/sqlite.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/system.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/timeseries.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/web_routes.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/routes/workers.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/API/utils.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Auth/__init__.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Auth/bootstrap.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Auth/migrate.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Auth/store.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Auth/workers.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/CacheRedis.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/CollectionMongoDB.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/DataAPI/__init__.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/DataAPI/catalog.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/DataAPI/config.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/DataAPI/errors.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/DataAPI/indexes.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/DataAPI/readers.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/DataAPI/resolve.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Database.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Defaults.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/AWSEC2.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/AWSS3.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/AutoDocstrings.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/ClientSocket.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/ClientWebSocket.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/LogHandlerAPI.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/LogReader.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/MongoDBClient.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/SaveTables.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/ServerSocket.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/ServerWebSocket.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/StreamsCache.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/StreamsPersist.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/SyncTable.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/TunnelWebSocket.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/IO/__init__.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Listing.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/LockWaiters.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/MCP/__init__.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/MCP/__main__.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/MCP/client.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/MCP/server.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Metadata.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/MultiProc.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/OpenFIGI.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Redaction.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/BatchJob.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/ContainerHealth.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/JobStore.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/Schedule.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/ScheduleMirror.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/ScheduleStore.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/Scheduler.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/Worker.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/WorkerPool.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Routines/__init__.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/SQLite.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/StreamKafka.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Symbol.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TableIndexJitHash.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TableIndexJitLoc.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TableProtocol.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TableRead.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TableRewrite.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TimeSeriesDisk.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/TimeseriesContainer.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Users.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/__init__.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/activity_logger.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/app.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/assets/google_logo.png +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/assets/kafka_keys.js +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/assets/logs.css +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/assets/sched.css +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/assets/styles.css +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/auth.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/backends.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/config.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/containers.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/identity.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/kafka_view.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/mongo_view.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/proc.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/redis_view.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/route_health.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/route_registry.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/sched.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/server.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/services.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/shell.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/standalone.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/account.js +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/custom.css +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/favicon.ico +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/favicon.png +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/logo.png +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/logo_96x96.png +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/shell.css +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/shell.js +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/standalone.css +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/static/standalone.js +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/suggest.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/account.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/base.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/claim.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/claim_done.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/dash.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/index.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/login.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/login_2fa.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/partials/navbar.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/partials/scripts.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/recovery_codes.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/shell.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/templates/standalone.html +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/twofactor.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/Web/utils.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/__init__.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/build_info.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/fleet.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/preflight.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/private_files.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/release_backup.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/server.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/services.py +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/SharedData/sharedmutexwin.pyd +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/shareddata.egg-info/dependency_links.txt +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/shareddata.egg-info/requires.txt +0 -0
- {shareddata-6.86.1 → shareddata-6.94.0}/src/shareddata.egg-info/top_level.txt +0 -0
|
@@ -66,7 +66,7 @@ test_requires = [
|
|
|
66
66
|
|
|
67
67
|
setup(
|
|
68
68
|
name='shareddata',
|
|
69
|
-
version='6.
|
|
69
|
+
version='6.94.0',
|
|
70
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',
|
|
@@ -92,6 +92,7 @@ setup(
|
|
|
92
92
|
extras_require={'test': test_requires, 's3': [], 'kafka': [], 'redis': [], 'all': []},
|
|
93
93
|
include_package_data=True,
|
|
94
94
|
package_data={
|
|
95
|
+
'SharedData.Analytics': ['data/cdie/*.csv', 'data/cdie/*.json'],
|
|
95
96
|
'SharedData.API': ['apidocs.yml', 'apidocs-dark.css', 'install/*'],
|
|
96
97
|
'SharedData.Web': [
|
|
97
98
|
'templates/*', 'templates/**/*',
|
|
@@ -105,6 +106,7 @@ setup(
|
|
|
105
106
|
'shdata-web=SharedData.Web.server:main',
|
|
106
107
|
'shdata-server=SharedData.server:main',
|
|
107
108
|
'shareddata-mcp=SharedData.MCP.server:main',
|
|
109
|
+
'shareddata-research-mcp=SharedData.ResearchMCP:main',
|
|
108
110
|
],
|
|
109
111
|
},
|
|
110
112
|
)
|
|
@@ -111,12 +111,16 @@ def create_app(shdata=None, quiet=True, app=None):
|
|
|
111
111
|
|
|
112
112
|
@app.before_request
|
|
113
113
|
def auth_availability():
|
|
114
|
+
from flask import g
|
|
114
115
|
from SharedData.Auth.store import StoreUnavailable
|
|
115
116
|
# Worker install scripts and enrollment take a one-time install code instead of a token.
|
|
116
117
|
public = request.path in ('/api/health', '/api/heartbeat', '/api/workers/enroll') or \
|
|
117
118
|
request.path.startswith(('/api/auth/', '/api/web/', '/api/workers/install/'))
|
|
118
119
|
from SharedData.API.auth import local_log_token_ok
|
|
119
|
-
if
|
|
120
|
+
if local_log_token_ok(request):
|
|
121
|
+
g.client_user = 'local-log'
|
|
122
|
+
return
|
|
123
|
+
if request.path.startswith('/api/') and not public:
|
|
120
124
|
try:
|
|
121
125
|
if not authenticate_fn(request, shdata):
|
|
122
126
|
return jsonify(error='unauthorized'), 401
|
|
@@ -126,6 +130,9 @@ def create_app(shdata=None, quiet=True, app=None):
|
|
|
126
130
|
# Register request lifecycle hooks
|
|
127
131
|
register_request_hooks(app)
|
|
128
132
|
|
|
133
|
+
from SharedData.API import access_log
|
|
134
|
+
access_log.register(app, shdata)
|
|
135
|
+
|
|
129
136
|
# Store shdata on app for access in routes
|
|
130
137
|
app.shdata = shdata
|
|
131
138
|
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
access_log.py — per-client API access logging, two-tier, never on the request path.
|
|
3
|
+
|
|
4
|
+
Tier 1 (always): per-minute aggregate counters keyed by
|
|
5
|
+
(user, route, status_class) — requests, bytes_in, bytes_out, total_ms, max_ms.
|
|
6
|
+
Small docs; powers bandwidth / usage / route-preference analytics.
|
|
7
|
+
|
|
8
|
+
Tier 2 (events): one document per request, but only for security-relevant or
|
|
9
|
+
sampled traffic — status >= 300, failed authentication, unmatched paths, plus a
|
|
10
|
+
configurable sample of successful requests.
|
|
11
|
+
|
|
12
|
+
Flush: a daemon thread drains both tiers every FLUSH_INTERVAL_S seconds and
|
|
13
|
+
writes through the in-process SharedData handle — Requests/RT/API/ACCESS_AGG
|
|
14
|
+
and Requests/RT/API/ACCESS_EVT (hasindex=False collections → insert_many).
|
|
15
|
+
No HTTP loopback: a ClientAPI.post_collection call would itself be a request
|
|
16
|
+
and self-log. Each server worker process flushes its own buffer (worker_pid).
|
|
17
|
+
|
|
18
|
+
Overhead: the request path does one dict update under a short lock and, for
|
|
19
|
+
events, a put_nowait — no I/O, no body reads. A full event queue drops the
|
|
20
|
+
record (counted in dropped_events). The request path never blocks on Mongo.
|
|
21
|
+
|
|
22
|
+
Secrets: the token itself is never logged — only token_fp (first 8 chars of the
|
|
23
|
+
server-side token_hash, stashed on flask.g by API/auth.py). The raw query
|
|
24
|
+
string is never logged either (it can carry ?token=); only the allow-listed
|
|
25
|
+
data-path arguments are kept.
|
|
26
|
+
|
|
27
|
+
Client IP: X-Real-IP / X-Forwarded-For are trusted only when the direct peer
|
|
28
|
+
is a known proxy; a LAN client hitting the API port directly cannot spoof its
|
|
29
|
+
address. Built in: loopback only (a same-host proxy). A proxy on another
|
|
30
|
+
address, or one that reaches the API by its LAN address, is listed in env
|
|
31
|
+
SHAREDDATA_ACCESSLOG_TRUSTED (comma-separated, replaces the built-in list).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
import queue
|
|
36
|
+
import random
|
|
37
|
+
import socket
|
|
38
|
+
import threading
|
|
39
|
+
import time
|
|
40
|
+
from collections import defaultdict
|
|
41
|
+
from datetime import datetime, timezone
|
|
42
|
+
|
|
43
|
+
from flask import g, request
|
|
44
|
+
|
|
45
|
+
from SharedData.Logger import Logger
|
|
46
|
+
|
|
47
|
+
# ─── Configuration ──────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
FLUSH_INTERVAL_S = float(os.environ.get('SHAREDDATA_ACCESSLOG_FLUSH_S', 30))
|
|
50
|
+
MAX_QUEUE_SIZE = int(os.environ.get('SHAREDDATA_ACCESSLOG_QUEUE', 10_000))
|
|
51
|
+
EVENT_SAMPLE = float(os.environ.get('SHAREDDATA_ACCESSLOG_SAMPLE', 0.01))
|
|
52
|
+
DISABLED = os.environ.get('SHAREDDATA_ACCESSLOG', '1') in ('0', 'false', 'off')
|
|
53
|
+
|
|
54
|
+
DATABASE = 'Requests'
|
|
55
|
+
PERIOD = 'RT'
|
|
56
|
+
SOURCE = 'API'
|
|
57
|
+
AGG_TABLE = 'ACCESS_AGG'
|
|
58
|
+
EVT_TABLE = 'ACCESS_EVT'
|
|
59
|
+
|
|
60
|
+
# Peers allowed to supply client addresses in X-Real-IP / X-Forwarded-For:
|
|
61
|
+
# loopback (same-host proxy), unless the deployment lists its own proxies.
|
|
62
|
+
_TRUSTED_PROXIES = frozenset(
|
|
63
|
+
part.strip() for part in (os.environ.get('SHAREDDATA_ACCESSLOG_TRUSTED') or '127.0.0.1,::1').split(',')
|
|
64
|
+
if part.strip()
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Public/polled endpoints and noise that is not worth a counter.
|
|
68
|
+
_SKIP_PREFIXES = (
|
|
69
|
+
'/api/health',
|
|
70
|
+
'/api/heartbeat',
|
|
71
|
+
'/flasgger_static',
|
|
72
|
+
'/favicon.ico',
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Data-path arguments worth keeping on event records (the object a client
|
|
76
|
+
# read/wrote). Everything else — most of all 'token' — stays out.
|
|
77
|
+
_DATA_ARGS = ('database', 'period', 'source', 'tablename', 'tablesubfolder',
|
|
78
|
+
'timeseries', 'symbol', 'user')
|
|
79
|
+
|
|
80
|
+
_PATH_MAX = 200
|
|
81
|
+
_UA_MAX = 160
|
|
82
|
+
|
|
83
|
+
# ─── Internal state ─────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
_evt_queue: 'queue.Queue[dict]' = queue.Queue(maxsize=MAX_QUEUE_SIZE)
|
|
86
|
+
|
|
87
|
+
# {(minute_epoch, user, route, status_class): [requests, bytes_in, bytes_out, total_ms, max_ms]}
|
|
88
|
+
_agg = defaultdict(lambda: [0, 0, 0, 0.0, 0.0])
|
|
89
|
+
_agg_lock = threading.Lock()
|
|
90
|
+
|
|
91
|
+
_pending_evts: list = [] # evt docs retained across a failed flush
|
|
92
|
+
_pending_lock = threading.Lock()
|
|
93
|
+
dropped_events = 0 # events lost to a full queue / retained-buffer cap
|
|
94
|
+
|
|
95
|
+
_shdata = None
|
|
96
|
+
_sink = None # fn(collection_name, docs); injectable for tests
|
|
97
|
+
_HOSTNAME = socket.gethostname()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _client_ip() -> str:
|
|
101
|
+
"""The real client address: the peer itself, unless the peer is a known
|
|
102
|
+
proxy that supplied X-Real-IP / X-Forwarded-For (original client first)."""
|
|
103
|
+
peer = request.remote_addr or ''
|
|
104
|
+
if peer in _TRUSTED_PROXIES:
|
|
105
|
+
real = request.headers.get('X-Real-IP')
|
|
106
|
+
if real:
|
|
107
|
+
return real.split(',')[0].strip()
|
|
108
|
+
xff = request.headers.get('X-Forwarded-For')
|
|
109
|
+
if xff:
|
|
110
|
+
return xff.split(',')[0].strip()
|
|
111
|
+
return peer or 'unknown'
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _user() -> str:
|
|
115
|
+
"""Auth identity resolved by API/auth.authenticate (or 'local-log' set by
|
|
116
|
+
the auth_availability hook). 'invalid-token' when a token was presented
|
|
117
|
+
and rejected, 'anonymous' when none was needed/presented."""
|
|
118
|
+
user = getattr(g, 'client_user', None)
|
|
119
|
+
if user:
|
|
120
|
+
return user
|
|
121
|
+
if getattr(g, 'auth_ok', None) is False:
|
|
122
|
+
return 'invalid-token'
|
|
123
|
+
return 'anonymous'
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _route() -> str:
|
|
127
|
+
rule = request.url_rule
|
|
128
|
+
return rule.rule if rule is not None else '<unmatched>'
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _record_event(response, ms, bytes_in, bytes_out):
|
|
132
|
+
rec = {
|
|
133
|
+
'kind': 'evt',
|
|
134
|
+
'ts': datetime.now(timezone.utc).isoformat(),
|
|
135
|
+
'host': _HOSTNAME,
|
|
136
|
+
'worker_pid': os.getpid(),
|
|
137
|
+
'remote_ip': _client_ip(),
|
|
138
|
+
'user': _user(),
|
|
139
|
+
'method': request.method,
|
|
140
|
+
'route': _route(),
|
|
141
|
+
'status': response.status_code,
|
|
142
|
+
'ms': round(ms, 3),
|
|
143
|
+
'bytes_in': bytes_in,
|
|
144
|
+
'bytes_out': bytes_out,
|
|
145
|
+
}
|
|
146
|
+
token_fp = getattr(g, 'token_fp', None)
|
|
147
|
+
if token_fp:
|
|
148
|
+
rec['token_fp'] = token_fp
|
|
149
|
+
if request.url_rule is None or response.status_code >= 300:
|
|
150
|
+
rec['path'] = request.path[:_PATH_MAX] # raw path: probes/errors only
|
|
151
|
+
ua = request.headers.get('User-Agent')
|
|
152
|
+
if ua:
|
|
153
|
+
rec['user_agent'] = ua[:_UA_MAX]
|
|
154
|
+
args = {k: request.args.get(k) for k in _DATA_ARGS if request.args.get(k) is not None}
|
|
155
|
+
if args:
|
|
156
|
+
rec['args'] = args
|
|
157
|
+
global dropped_events
|
|
158
|
+
try:
|
|
159
|
+
_evt_queue.put_nowait(rec)
|
|
160
|
+
except queue.Full:
|
|
161
|
+
dropped_events += 1
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ─── Flask hooks ────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
def before_request():
|
|
167
|
+
g._accesslog_start = time.perf_counter()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def after_request(response):
|
|
171
|
+
try:
|
|
172
|
+
path = request.path
|
|
173
|
+
if any(path.startswith(p) for p in _SKIP_PREFIXES):
|
|
174
|
+
return response
|
|
175
|
+
|
|
176
|
+
start = getattr(g, '_accesslog_start', None)
|
|
177
|
+
ms = (time.perf_counter() - start) * 1000.0 if start is not None else 0.0
|
|
178
|
+
status = response.status_code
|
|
179
|
+
|
|
180
|
+
content_length = response.calculate_content_length()
|
|
181
|
+
bytes_out = content_length if content_length is not None else 0
|
|
182
|
+
bytes_in = g.get('request_bytes', request.content_length or 0)
|
|
183
|
+
|
|
184
|
+
minute = int(time.time() // 60) * 60
|
|
185
|
+
key = (minute, _user(), _route(), f'{status // 100}xx')
|
|
186
|
+
with _agg_lock:
|
|
187
|
+
bucket = _agg[key]
|
|
188
|
+
bucket[0] += 1
|
|
189
|
+
bucket[1] += bytes_in
|
|
190
|
+
bucket[2] += bytes_out
|
|
191
|
+
bucket[3] += ms
|
|
192
|
+
if ms > bucket[4]:
|
|
193
|
+
bucket[4] = ms
|
|
194
|
+
|
|
195
|
+
if status >= 300 or getattr(g, 'auth_ok', None) is False \
|
|
196
|
+
or random.random() < EVENT_SAMPLE:
|
|
197
|
+
_record_event(response, ms, bytes_in, bytes_out)
|
|
198
|
+
|
|
199
|
+
except Exception as exc: # logging must never break the app
|
|
200
|
+
try:
|
|
201
|
+
Logger.log.warning(f'access_log after_request: {exc}')
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
return response
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ─── Flush ──────────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
def _agg_docs(snapshot):
|
|
211
|
+
docs = []
|
|
212
|
+
for (minute, user, route, status_class), (n, bi, bo, total_ms, max_ms) in snapshot.items():
|
|
213
|
+
docs.append({
|
|
214
|
+
'kind': 'agg',
|
|
215
|
+
'ts': datetime.fromtimestamp(minute, tz=timezone.utc).isoformat(),
|
|
216
|
+
'host': _HOSTNAME,
|
|
217
|
+
'worker_pid': os.getpid(),
|
|
218
|
+
'user': user,
|
|
219
|
+
'route': route,
|
|
220
|
+
'status_class': status_class,
|
|
221
|
+
'requests': n,
|
|
222
|
+
'bytes_in': bi,
|
|
223
|
+
'bytes_out': bo,
|
|
224
|
+
'total_ms': round(total_ms, 3),
|
|
225
|
+
'max_ms': round(max_ms, 3),
|
|
226
|
+
})
|
|
227
|
+
return docs
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _flush_once(sink):
|
|
231
|
+
"""Drain both tiers and hand them to sink(name, docs). Returns doc counts.
|
|
232
|
+
On sink failure event docs are retained for the next cycle (capped);
|
|
233
|
+
aggregate buckets are minute-resolution and simply lost on failure."""
|
|
234
|
+
global dropped_events, _agg
|
|
235
|
+
with _agg_lock:
|
|
236
|
+
snapshot = _agg
|
|
237
|
+
_agg = defaultdict(lambda: [0, 0, 0, 0.0, 0.0])
|
|
238
|
+
with _pending_lock:
|
|
239
|
+
evts = list(_pending_evts)
|
|
240
|
+
_pending_evts.clear()
|
|
241
|
+
while True:
|
|
242
|
+
try:
|
|
243
|
+
evts.append(_evt_queue.get_nowait())
|
|
244
|
+
except queue.Empty:
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
agg_docs = _agg_docs(snapshot)
|
|
248
|
+
if not agg_docs and not evts:
|
|
249
|
+
return 0, 0
|
|
250
|
+
try:
|
|
251
|
+
if agg_docs:
|
|
252
|
+
sink(AGG_TABLE, agg_docs)
|
|
253
|
+
if evts:
|
|
254
|
+
sink(EVT_TABLE, evts)
|
|
255
|
+
return len(agg_docs), len(evts)
|
|
256
|
+
except Exception as exc:
|
|
257
|
+
Logger.log.error(f'access_log flush error: {exc}')
|
|
258
|
+
with _pending_lock:
|
|
259
|
+
keep = _pending_evts + evts
|
|
260
|
+
if len(keep) > MAX_QUEUE_SIZE:
|
|
261
|
+
dropped_events += len(keep) - MAX_QUEUE_SIZE
|
|
262
|
+
keep = keep[-MAX_QUEUE_SIZE:]
|
|
263
|
+
_pending_evts[:] = keep
|
|
264
|
+
# aggs are minute-buckets; on failure they are simply lost (bounded)
|
|
265
|
+
return 0, 0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _default_sink(name, docs):
|
|
269
|
+
coll = _shdata.collection(DATABASE, PERIOD, SOURCE, name,
|
|
270
|
+
hasindex=False, user='master')
|
|
271
|
+
coll.extend(docs)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _flush_worker():
|
|
275
|
+
while True:
|
|
276
|
+
time.sleep(FLUSH_INTERVAL_S)
|
|
277
|
+
_flush_once(_sink or _default_sink)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ─── Registration ───────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
def register(flask_app, shdata_instance, sink=None):
|
|
283
|
+
"""Attach access logging to a Flask app; starts the flush thread once.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
flask_app : Flask application instance
|
|
288
|
+
shdata_instance : SharedData handle for the direct collection write
|
|
289
|
+
sink : optional fn(name, docs) override (tests)
|
|
290
|
+
"""
|
|
291
|
+
global _shdata, _sink
|
|
292
|
+
if DISABLED:
|
|
293
|
+
Logger.log.info('access_log: disabled (SHAREDDATA_ACCESSLOG=0)')
|
|
294
|
+
return
|
|
295
|
+
if flask_app.extensions.get('shareddata_access_log'):
|
|
296
|
+
return
|
|
297
|
+
_shdata = shdata_instance
|
|
298
|
+
if sink is not None:
|
|
299
|
+
_sink = sink
|
|
300
|
+
flask_app.before_request(before_request)
|
|
301
|
+
flask_app.after_request(after_request)
|
|
302
|
+
flask_app.extensions['shareddata_access_log'] = True
|
|
303
|
+
threading.Thread(target=_flush_worker, daemon=True,
|
|
304
|
+
name='api-access-log-flush').start()
|
|
305
|
+
Logger.log.info('access_log: registered (flush every %ds, event sample %.3f)',
|
|
306
|
+
FLUSH_INTERVAL_S, EVENT_SAMPLE)
|
|
@@ -120,17 +120,21 @@ def check_permissions(reqpath: list, permissions: dict, method: str, identity=No
|
|
|
120
120
|
def authenticate(request, shdata) -> bool:
|
|
121
121
|
"""
|
|
122
122
|
Authenticate an incoming HTTP request by validating a token and verifying permissions.
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
Parameters:
|
|
125
125
|
request (flask.Request): The incoming HTTP request object.
|
|
126
126
|
shdata: SharedData instance for accessing user table.
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
Returns:
|
|
129
129
|
bool: True if authenticated and authorized; False otherwise.
|
|
130
130
|
"""
|
|
131
|
+
from flask import g
|
|
131
132
|
from SharedData.Auth.store import get_store, StoreUnavailable, token_hash
|
|
132
133
|
token = get_token_from_request(request)
|
|
134
|
+
if token:
|
|
135
|
+
g.token_fp = token_hash(token)[:8]
|
|
133
136
|
if not token:
|
|
137
|
+
g.auth_ok = False
|
|
134
138
|
time.sleep(AUTH_FAILURE_SLEEP_SECONDS)
|
|
135
139
|
return False
|
|
136
140
|
try:
|
|
@@ -138,8 +142,12 @@ def authenticate(request, shdata) -> bool:
|
|
|
138
142
|
except Exception as exc:
|
|
139
143
|
raise StoreUnavailable('auth store lookup failed') from exc
|
|
140
144
|
if not userdata:
|
|
145
|
+
g.auth_ok = False
|
|
146
|
+
g.client_user = 'invalid-token'
|
|
141
147
|
time.sleep(AUTH_FAILURE_SLEEP_SECONDS)
|
|
142
148
|
return False
|
|
149
|
+
g.auth_ok = True
|
|
150
|
+
g.client_user = userdata.get('email') or userdata.get('symbol') or 'unknown'
|
|
143
151
|
with user_by_token_lock:
|
|
144
152
|
if len(user_by_token) >= 4096:
|
|
145
153
|
user_by_token.clear()
|
|
@@ -14,7 +14,7 @@ from SharedData.Auth.store import StoreUnavailable
|
|
|
14
14
|
from SharedData.DataAPI import service
|
|
15
15
|
from SharedData.DataAPI.errors import DataAPIError
|
|
16
16
|
|
|
17
|
-
KEYED = ('symbol', 'marketdata', 'signals', 'portfolios', 'riskfactors', 'vol')
|
|
17
|
+
KEYED = ('symbol', 'marketdata', 'signals', 'portfolios', 'riskfactors', 'vol', 'cdie')
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def token_permissions(request):
|
|
@@ -16,6 +16,7 @@ from SharedData.CollectionMongoDB import CollectionMongoDB
|
|
|
16
16
|
from SharedData.Database import DATABASE_PKEYS
|
|
17
17
|
from SharedData.Table import TableNotFound
|
|
18
18
|
from SharedData.SharedNumpy import IndexNotAvailable
|
|
19
|
+
from SharedData.TableVector import scalar_names
|
|
19
20
|
from SharedData.TableRead import generation_read
|
|
20
21
|
from SharedData.Listing import ListingBudget, ListingTimeout, ListingTooLarge
|
|
21
22
|
|
|
@@ -218,9 +219,16 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
218
219
|
since = np.datetime64(since.value, 'ns')
|
|
219
220
|
plain = not (startdate or enddate or symbols or portfolios or tags or query)
|
|
220
221
|
|
|
222
|
+
if not tbl.count:
|
|
223
|
+
_require_filter_indexes(tbl, startdate, enddate, symbols, portfolios, tags)
|
|
224
|
+
return make_empty_response(204)
|
|
225
|
+
|
|
221
226
|
loc = None
|
|
222
227
|
|
|
223
|
-
if
|
|
228
|
+
if symbols is not None:
|
|
229
|
+
# symbol first: the symbols' own rows filtered by date, instead of every row in the date range
|
|
230
|
+
loc = tbl.get_symbols_date_loc(symbols.split(','), startdate, enddate)
|
|
231
|
+
elif startdate is not None and enddate is not None:
|
|
224
232
|
loc = tbl.get_date_loc_gte_lte(startdate, enddate)
|
|
225
233
|
elif startdate is not None:
|
|
226
234
|
loc = tbl.get_date_loc_gte(startdate)
|
|
@@ -238,18 +246,6 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
238
246
|
_require_filter_indexes(tbl, startdate, enddate, symbols, portfolios, tags)
|
|
239
247
|
return make_empty_response(204)
|
|
240
248
|
|
|
241
|
-
# filter data by symbols
|
|
242
|
-
if symbols is not None:
|
|
243
|
-
symbols = symbols.split(',')
|
|
244
|
-
symbolloc = []
|
|
245
|
-
for symbol in symbols:
|
|
246
|
-
symbolloc.extend(tbl.get_symbol_loc(symbol))
|
|
247
|
-
symbolloc = np.array(symbolloc)
|
|
248
|
-
if len(symbolloc) > 0:
|
|
249
|
-
loc = np.intersect1d(loc, symbolloc)
|
|
250
|
-
else:
|
|
251
|
-
loc = np.array([])
|
|
252
|
-
|
|
253
249
|
# filter data by portfolios
|
|
254
250
|
if portfolios is not None:
|
|
255
251
|
portfolios = portfolios.split(',')
|
|
@@ -278,8 +274,13 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
278
274
|
loc = np.array(loc)
|
|
279
275
|
|
|
280
276
|
# cycle query keys
|
|
281
|
-
if query
|
|
277
|
+
if not isinstance(query, dict):
|
|
278
|
+
return jsonify({'error': 'query must be a JSON object'}), 400
|
|
279
|
+
if query:
|
|
280
|
+
scalars = set(scalar_names(tbl.dtype))
|
|
282
281
|
for key in query.keys():
|
|
282
|
+
if key not in scalars:
|
|
283
|
+
return jsonify({'error': f'Unknown filter column: {key}'}), 400
|
|
283
284
|
# Take the column view FIRST, then index it.
|
|
284
285
|
#
|
|
285
286
|
# `tbl[key]` is zero-copy (SharedNumpy.__getitem__ slices to
|
|
@@ -342,8 +343,11 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
342
343
|
# projection for schemaless tables is applied to the decoded dict_list
|
|
343
344
|
# further down instead.
|
|
344
345
|
if columns and not tbl.is_schemaless:
|
|
345
|
-
|
|
346
|
-
|
|
346
|
+
requested = [c for c in columns.split(',') if c and (c not in tbl.dtype.fields or tbl.dtype[c].subdtype is None)]
|
|
347
|
+
bad = [c for c in requested if c not in tbl.dtype.fields]
|
|
348
|
+
if bad:
|
|
349
|
+
return jsonify({'error': f'Unknown column: {bad[0]}'}), 400
|
|
350
|
+
columns = np.array([c for c in requested if c not in pkey])
|
|
347
351
|
columns = pkey + list(np.unique(columns))
|
|
348
352
|
names = columns
|
|
349
353
|
formats = [tbl.dtype.fields[name][0].str for name in names]
|
|
@@ -423,6 +427,10 @@ def init_tables_routes(shdata, authenticate_fn):
|
|
|
423
427
|
response.headers['Content-Length'] = len(response_data_json)
|
|
424
428
|
return sent(response)
|
|
425
429
|
else: # output_format=='bin'
|
|
430
|
+
if any(recs2send.dtype[name].subdtype is not None for name in recs2send.dtype.names):
|
|
431
|
+
# a vector column stays local: the wire layout carries one value per field per row
|
|
432
|
+
from numpy.lib import recfunctions
|
|
433
|
+
recs2send = recfunctions.repack_fields(recs2send[scalar_names(recs2send.dtype)])
|
|
426
434
|
names = list(recs2send.dtype.names)
|
|
427
435
|
formats = [recs2send.dtype.fields[name][0].str for name in names]
|
|
428
436
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Pure analytical calculations; importing this package does not connect to services."""
|