shareddata 6.86.0__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.94.0/PKG-INFO +164 -0
- shareddata-6.94.0/README.md +89 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/setup.py +4 -2
- {shareddata-6.86.0 → 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.0 → shareddata-6.94.0}/src/SharedData/API/apidocs.yml +1 -1
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/auth.py +10 -2
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/data.py +1 -1
- {shareddata-6.86.0 → 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.0 → shareddata-6.94.0}/src/SharedData/DataAPI/service.py +6 -1
- {shareddata-6.86.0 → 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.0 → 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.0 → shareddata-6.94.0}/src/SharedData/Routines/RoutineCommand.py +8 -1
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/RoutineOrigin.py +6 -1
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/WorkerLib.py +28 -3
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/SharedData.py +21 -4
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/SharedNumpy.py +124 -8
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Table.py +120 -27
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TableDisk.py +5 -3
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TableIndex.py +96 -38
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TableIndexJitFunctions.py +1247 -48
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TableIndexJitGenerate.py +34 -14
- shareddata-6.94.0/src/SharedData/TableVector.py +140 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Utils.py +4 -2
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/schedule_editor.py +9 -2
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/workerpool.py +6 -1
- shareddata-6.94.0/src/shareddata.egg-info/PKG-INFO +164 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/shareddata.egg-info/SOURCES.txt +25 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/shareddata.egg-info/entry_points.txt +1 -0
- shareddata-6.86.0/PKG-INFO +0 -245
- shareddata-6.86.0/README.md +0 -170
- shareddata-6.86.0/src/shareddata.egg-info/PKG-INFO +0 -245
- {shareddata-6.86.0 → shareddata-6.94.0}/LICENSE +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/MANIFEST.in +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/pyproject.toml +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/setup.cfg +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/ServerGunicorn.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/ServerWaitress.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/apidocs-dark.css +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/constants.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/download_paths.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/download_slots.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/install/worker.ps1 +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/install/worker.sh +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/listcache.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/__init__.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/cache.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/collections.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/downloads.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/metadata.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/operations.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/schedules.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/sqlite.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/system.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/timeseries.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/web_routes.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/routes/workers.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/API/utils.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Auth/__init__.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Auth/bootstrap.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Auth/migrate.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Auth/store.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Auth/workers.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/CacheRedis.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/CollectionMongoDB.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/DataAPI/__init__.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/DataAPI/catalog.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/DataAPI/config.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/DataAPI/errors.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/DataAPI/indexes.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/DataAPI/readers.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/DataAPI/resolve.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Database.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Defaults.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/AWSEC2.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/AWSS3.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/AutoDocstrings.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/ClientSocket.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/ClientWebSocket.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/LogHandlerAPI.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/LogReader.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/MongoDBClient.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/SaveTables.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/ServerSocket.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/ServerWebSocket.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/StreamsCache.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/StreamsPersist.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/SyncTable.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/TunnelWebSocket.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/IO/__init__.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Listing.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/LockWaiters.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/MCP/__init__.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/MCP/__main__.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/MCP/client.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/MCP/server.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Metadata.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/MultiProc.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/OpenFIGI.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Redaction.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/BatchJob.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/ContainerHealth.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/JobStore.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/Schedule.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/ScheduleMirror.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/ScheduleStore.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/Scheduler.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/Worker.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/WorkerPool.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Routines/__init__.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/SQLite.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/StreamKafka.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Symbol.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TableIndexJitHash.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TableIndexJitLoc.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TableProtocol.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TableRead.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TableRewrite.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TimeSeriesDisk.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/TimeseriesContainer.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Users.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/__init__.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/activity_logger.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/app.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/assets/google_logo.png +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/assets/kafka_keys.js +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/assets/logs.css +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/assets/sched.css +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/assets/styles.css +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/auth.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/backends.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/config.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/containers.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/identity.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/kafka_view.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/mongo_view.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/proc.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/redis_view.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/route_health.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/route_registry.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/sched.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/server.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/services.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/shell.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/standalone.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/account.js +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/custom.css +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/favicon.ico +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/favicon.png +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/logo.png +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/logo_96x96.png +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/shell.css +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/shell.js +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/standalone.css +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/static/standalone.js +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/suggest.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/account.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/base.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/claim.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/claim_done.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/dash.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/index.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/login.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/login_2fa.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/partials/navbar.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/partials/scripts.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/recovery_codes.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/shell.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/templates/standalone.html +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/twofactor.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/Web/utils.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/__init__.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/build_info.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/fleet.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/preflight.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/private_files.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/release_backup.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/server.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/services.py +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/SharedData/sharedmutexwin.pyd +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/shareddata.egg-info/dependency_links.txt +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/shareddata.egg-info/requires.txt +0 -0
- {shareddata-6.86.0 → shareddata-6.94.0}/src/shareddata.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shareddata
|
|
3
|
+
Version: 6.94.0
|
|
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.
|
|
70
|
-
description='Memory
|
|
69
|
+
version='6.94.0',
|
|
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',
|
|
@@ -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):
|