shareddata 6.86.2__tar.gz → 6.99.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.86.2/src/shareddata.egg-info → shareddata-6.99.0}/PKG-INFO +1 -1
- {shareddata-6.86.2 → shareddata-6.99.0}/setup.py +3 -1
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/__init__.py +8 -1
- shareddata-6.99.0/src/SharedData/API/access_log.py +306 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/apidocs.yml +243 -1
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/auth.py +10 -2
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/schedules.py +74 -6
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/tables.py +25 -17
- shareddata-6.99.0/src/SharedData/Charts/Server.py +240 -0
- shareddata-6.99.0/src/SharedData/Charts/__init__.py +1 -0
- shareddata-6.99.0/src/SharedData/Charts/auth.py +84 -0
- shareddata-6.99.0/src/SharedData/Charts/cache.py +66 -0
- shareddata-6.99.0/src/SharedData/Charts/errors.py +4 -0
- shareddata-6.99.0/src/SharedData/Charts/index.py +190 -0
- shareddata-6.99.0/src/SharedData/Charts/lifecycle.py +44 -0
- shareddata-6.99.0/src/SharedData/Charts/memory.py +49 -0
- shareddata-6.99.0/src/SharedData/Charts/periods.py +61 -0
- shareddata-6.99.0/src/SharedData/Charts/schedule.py +82 -0
- shareddata-6.99.0/src/SharedData/Charts/series.py +83 -0
- shareddata-6.99.0/src/SharedData/Charts/service.py +334 -0
- shareddata-6.99.0/src/SharedData/Charts/templates/tradingview.html +16 -0
- shareddata-6.99.0/src/SharedData/Charts/tradingview.py +89 -0
- shareddata-6.99.0/src/SharedData/ConsensusGraph.py +869 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/DataAPI/resolve.py +45 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Defaults.py +31 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/ClientAPI.py +146 -51
- shareddata-6.99.0/src/SharedData/KnowledgeBase.py +1474 -0
- shareddata-6.99.0/src/SharedData/KnowledgeContract.py +978 -0
- shareddata-6.99.0/src/SharedData/KnowledgeEvaluation.py +232 -0
- shareddata-6.99.0/src/SharedData/KnowledgeExperiment.py +335 -0
- shareddata-6.99.0/src/SharedData/KnowledgeGraph.py +536 -0
- shareddata-6.99.0/src/SharedData/KnowledgeLookup.py +156 -0
- shareddata-6.99.0/src/SharedData/KnowledgeMarket.py +417 -0
- shareddata-6.99.0/src/SharedData/KnowledgeOntology.py +176 -0
- shareddata-6.99.0/src/SharedData/KnowledgeResearch.py +481 -0
- shareddata-6.99.0/src/SharedData/KnowledgeScenario.py +200 -0
- shareddata-6.99.0/src/SharedData/KnowledgeXBRL.py +273 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Logger.py +4 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/MCP/server.py +3 -7
- shareddata-6.99.0/src/SharedData/ResearchMCP.py +432 -0
- shareddata-6.99.0/src/SharedData/Routines/HealthProbe.py +100 -0
- shareddata-6.99.0/src/SharedData/Routines/PackageRoutine.py +235 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/RoutineCommand.py +8 -1
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/RoutineOrigin.py +6 -1
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/Schedule.py +19 -4
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/ScheduleStore.py +1 -1
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/WorkerLib.py +30 -4
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/SharedData.py +21 -4
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/SharedNumpy.py +135 -8
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Table.py +169 -30
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TableDisk.py +5 -3
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TableIndex.py +277 -120
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TableIndexJitFunctions.py +1247 -48
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TableIndexJitGenerate.py +34 -14
- shareddata-6.99.0/src/SharedData/TableVector.py +140 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Utils.py +4 -2
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/proc.py +1 -1
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/sched.py +2 -0
- shareddata-6.99.0/src/SharedData/Web/schedule_editor.py +365 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/workerpool.py +6 -1
- {shareddata-6.86.2 → shareddata-6.99.0/src/shareddata.egg-info}/PKG-INFO +1 -1
- {shareddata-6.86.2 → shareddata-6.99.0}/src/shareddata.egg-info/SOURCES.txt +31 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/shareddata.egg-info/entry_points.txt +1 -0
- shareddata-6.86.2/src/SharedData/Web/schedule_editor.py +0 -156
- {shareddata-6.86.2 → shareddata-6.99.0}/LICENSE +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/MANIFEST.in +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/README.md +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/pyproject.toml +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/setup.cfg +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/ServerGunicorn.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/ServerWaitress.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/apidocs-dark.css +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/constants.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/download_paths.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/download_slots.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/install/worker.ps1 +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/install/worker.sh +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/listcache.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/__init__.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/cache.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/collections.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/data.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/downloads.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/metadata.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/operations.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/sqlite.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/system.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/timeseries.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/web_routes.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/routes/workers.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/API/utils.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Auth/__init__.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Auth/bootstrap.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Auth/migrate.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Auth/store.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Auth/workers.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/CacheRedis.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/CollectionMongoDB.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/DataAPI/__init__.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/DataAPI/catalog.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/DataAPI/config.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/DataAPI/errors.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/DataAPI/indexes.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/DataAPI/readers.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/DataAPI/service.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Database.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/AWSEC2.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/AWSS3.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/AutoDocstrings.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/ClientSocket.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/ClientWebSocket.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/LogHandlerAPI.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/LogReader.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/MongoDBClient.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/SaveTables.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/ServerSocket.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/ServerWebSocket.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/StreamsCache.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/StreamsPersist.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/SyncTable.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/TunnelWebSocket.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/IO/__init__.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Listing.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/LockWaiters.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/MCP/__init__.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/MCP/__main__.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/MCP/client.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Metadata.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/MultiProc.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/OpenFIGI.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Redaction.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/BatchJob.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/ContainerHealth.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/JobStore.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/ScheduleMirror.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/Scheduler.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/Worker.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/WorkerPool.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Routines/__init__.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/SQLite.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/StreamKafka.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Symbol.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TableIndexJitHash.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TableIndexJitLoc.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TableProtocol.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TableRead.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TableRewrite.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TimeSeriesDisk.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/TimeseriesContainer.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Users.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/__init__.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/activity_logger.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/app.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/assets/google_logo.png +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/assets/kafka_keys.js +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/assets/logs.css +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/assets/sched.css +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/assets/styles.css +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/auth.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/backends.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/config.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/containers.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/identity.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/kafka_view.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/mongo_view.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/redis_view.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/route_health.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/route_registry.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/server.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/services.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/shell.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/standalone.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/account.js +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/custom.css +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/favicon.ico +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/favicon.png +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/logo.png +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/logo_96x96.png +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/shell.css +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/shell.js +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/standalone.css +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/static/standalone.js +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/suggest.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/account.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/base.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/claim.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/claim_done.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/dash.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/index.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/login.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/login_2fa.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/partials/navbar.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/partials/scripts.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/recovery_codes.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/shell.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/templates/standalone.html +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/twofactor.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/Web/utils.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/__init__.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/build_info.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/fleet.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/preflight.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/private_files.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/release_backup.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/server.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/services.py +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/SharedData/sharedmutexwin.pyd +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/shareddata.egg-info/dependency_links.txt +0 -0
- {shareddata-6.86.2 → shareddata-6.99.0}/src/shareddata.egg-info/requires.txt +0 -0
- {shareddata-6.86.2 → shareddata-6.99.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.99.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.Charts': ['templates/*'],
|
|
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)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
openapi: 3.0.3
|
|
3
3
|
info:
|
|
4
4
|
title: SharedData API
|
|
5
|
-
version: 6.
|
|
5
|
+
version: 6.99.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(...)`).
|
|
@@ -128,6 +128,95 @@ components:
|
|
|
128
128
|
in: query
|
|
129
129
|
name: token
|
|
130
130
|
description: API token in the URL (avoid).
|
|
131
|
+
schemas:
|
|
132
|
+
ScheduleRoutine:
|
|
133
|
+
type: object
|
|
134
|
+
properties:
|
|
135
|
+
routine:
|
|
136
|
+
type: string
|
|
137
|
+
description: user@COMPUTER:script, the routine's name
|
|
138
|
+
user:
|
|
139
|
+
type: string
|
|
140
|
+
computer:
|
|
141
|
+
type: string
|
|
142
|
+
script:
|
|
143
|
+
type: string
|
|
144
|
+
description: Repo/folder/code, Repo#branch/folder/code or SharedData.Module
|
|
145
|
+
group:
|
|
146
|
+
type: string
|
|
147
|
+
realtime:
|
|
148
|
+
type: boolean
|
|
149
|
+
external:
|
|
150
|
+
type: boolean
|
|
151
|
+
runtimes:
|
|
152
|
+
type: string
|
|
153
|
+
description: HH:MM,HH:MM (server local time); empty for none
|
|
154
|
+
dependencies:
|
|
155
|
+
type: string
|
|
156
|
+
description: Comma-separated routine names
|
|
157
|
+
args:
|
|
158
|
+
type: string
|
|
159
|
+
description: Masked when a flag's name suggests a secret
|
|
160
|
+
args_masked:
|
|
161
|
+
type: boolean
|
|
162
|
+
health_url:
|
|
163
|
+
type: string
|
|
164
|
+
ScheduleRoutineEdit:
|
|
165
|
+
type: object
|
|
166
|
+
additionalProperties: false
|
|
167
|
+
required:
|
|
168
|
+
- revision
|
|
169
|
+
properties:
|
|
170
|
+
revision:
|
|
171
|
+
type: string
|
|
172
|
+
description: The revision GET /api/schedules/{name}/routines answered (64 hex characters)
|
|
173
|
+
user:
|
|
174
|
+
type: string
|
|
175
|
+
maxLength: 100
|
|
176
|
+
pattern: ^[A-Za-z0-9_.+-]+$
|
|
177
|
+
description: Required for POST and PUT
|
|
178
|
+
computer:
|
|
179
|
+
type: string
|
|
180
|
+
maxLength: 100
|
|
181
|
+
pattern: ^[A-Za-z0-9_.+-]+$
|
|
182
|
+
description: Required for POST and PUT
|
|
183
|
+
script:
|
|
184
|
+
type: string
|
|
185
|
+
maxLength: 600
|
|
186
|
+
description: Required for POST and PUT. Repo/folder/code[.py], Repo#branch/folder/code or SharedData.Module;
|
|
187
|
+
a path inside its repository
|
|
188
|
+
group:
|
|
189
|
+
type: string
|
|
190
|
+
maxLength: 2000
|
|
191
|
+
realtime:
|
|
192
|
+
type: boolean
|
|
193
|
+
external:
|
|
194
|
+
type: boolean
|
|
195
|
+
runtimes:
|
|
196
|
+
type: string
|
|
197
|
+
description: HH:MM, distinct, comma-separated; empty for none
|
|
198
|
+
dependencies:
|
|
199
|
+
oneOf:
|
|
200
|
+
- type: string
|
|
201
|
+
- type: array
|
|
202
|
+
items:
|
|
203
|
+
type: string
|
|
204
|
+
description: Routine names (user@COMPUTER:script, _HHMM for one run time of a routine with several), each
|
|
205
|
+
in the schedule, distinct, not later than this routine, no cycle
|
|
206
|
+
args:
|
|
207
|
+
type: string
|
|
208
|
+
maxLength: 20000
|
|
209
|
+
description: One line, single spaces between arguments (the Worker does not interpret quotes). Left out,
|
|
210
|
+
or sent back masked exactly as read, keeps the stored arguments
|
|
211
|
+
health_url:
|
|
212
|
+
type: string
|
|
213
|
+
description: Realtime routines only; http(s) on localhost or a private IP, no credentials, query or fragment
|
|
214
|
+
routine:
|
|
215
|
+
type: string
|
|
216
|
+
description: Read-only; may be sent back, then it must be the routine edited
|
|
217
|
+
args_masked:
|
|
218
|
+
type: boolean
|
|
219
|
+
description: Read-only; ignored
|
|
131
220
|
parameters:
|
|
132
221
|
Token:
|
|
133
222
|
in: query
|
|
@@ -1727,6 +1816,29 @@ paths:
|
|
|
1727
1816
|
description: No such schedule here
|
|
1728
1817
|
description: 'What the SCHED page shows, for a script: the routines in run order, each with its status as the
|
|
1729
1818
|
Scheduler keeps it, a level, its last message and how many seconds ago it spoke (`age`).'
|
|
1819
|
+
delete:
|
|
1820
|
+
tags:
|
|
1821
|
+
- Workers
|
|
1822
|
+
summary: Remove every routine of a schedule; it stays kept, empty (master token)
|
|
1823
|
+
parameters:
|
|
1824
|
+
- name: revision
|
|
1825
|
+
in: query
|
|
1826
|
+
description: The revision read, when it is not sent as {"revision"} in the body
|
|
1827
|
+
schema:
|
|
1828
|
+
type: string
|
|
1829
|
+
responses:
|
|
1830
|
+
'200':
|
|
1831
|
+
description: '{schedule, revision, kept, routines: []}'
|
|
1832
|
+
'400':
|
|
1833
|
+
description: Revision missing
|
|
1834
|
+
'403':
|
|
1835
|
+
description: Master token required
|
|
1836
|
+
'404':
|
|
1837
|
+
description: No such schedule here
|
|
1838
|
+
'409':
|
|
1839
|
+
description: The schedule changed since its revision was read
|
|
1840
|
+
description: 'The schedule is kept empty, so its old copy in the metadata store is never imported again.
|
|
1841
|
+
Running processes are not stopped.'
|
|
1730
1842
|
/api/workers:
|
|
1731
1843
|
get:
|
|
1732
1844
|
tags:
|
|
@@ -1874,6 +1986,136 @@ paths:
|
|
|
1874
1986
|
description: 'Takes effect within a second and stays until switched back, also across a restart of the
|
|
1875
1987
|
Scheduler. While a schedule is on `observe` the Scheduler goes on keeping the status of its routines; a
|
|
1876
1988
|
routine can still be restarted or stopped by a person. Master token.'
|
|
1989
|
+
/api/schedules/{name}/routines:
|
|
1990
|
+
parameters:
|
|
1991
|
+
- name: name
|
|
1992
|
+
in: path
|
|
1993
|
+
required: true
|
|
1994
|
+
description: Schedule name, e.g. MASTER
|
|
1995
|
+
schema:
|
|
1996
|
+
type: string
|
|
1997
|
+
get:
|
|
1998
|
+
tags:
|
|
1999
|
+
- Workers
|
|
2000
|
+
summary: The definition of a schedule, every routine with the revision an edit sends back (master token)
|
|
2001
|
+
responses:
|
|
2002
|
+
'200':
|
|
2003
|
+
description: '{schedule, revision, kept, routines: [ScheduleRoutine]}. kept=false: no such schedule
|
|
2004
|
+
yet, its first routine creates it.'
|
|
2005
|
+
'400':
|
|
2006
|
+
description: Invalid schedule name
|
|
2007
|
+
'403':
|
|
2008
|
+
description: Master token required
|
|
2009
|
+
post:
|
|
2010
|
+
tags:
|
|
2011
|
+
- Workers
|
|
2012
|
+
summary: Add a routine to a schedule; the first one creates the schedule (master token)
|
|
2013
|
+
requestBody:
|
|
2014
|
+
required: true
|
|
2015
|
+
content:
|
|
2016
|
+
application/json:
|
|
2017
|
+
schema:
|
|
2018
|
+
$ref: '#/components/schemas/ScheduleRoutineEdit'
|
|
2019
|
+
responses:
|
|
2020
|
+
'201':
|
|
2021
|
+
description: '{schedule, revision, routine: ScheduleRoutine}'
|
|
2022
|
+
'400':
|
|
2023
|
+
description: A field is wrong (the error says which), revision missing, or a dependency is missing,
|
|
2024
|
+
later than the routine or makes a cycle. Nothing was written.
|
|
2025
|
+
'403':
|
|
2026
|
+
description: Master token required
|
|
2027
|
+
'409':
|
|
2028
|
+
description: The schedule changed since its revision was read, or the routine already exists
|
|
2029
|
+
description: 'Every field is checked before the store is touched. The Scheduler takes the routine up on its
|
|
2030
|
+
next tick; nothing is started by the edit itself.'
|
|
2031
|
+
/api/schedules/{name}/routines/{routine}:
|
|
2032
|
+
parameters:
|
|
2033
|
+
- name: name
|
|
2034
|
+
in: path
|
|
2035
|
+
required: true
|
|
2036
|
+
description: Schedule name, e.g. MASTER
|
|
2037
|
+
schema:
|
|
2038
|
+
type: string
|
|
2039
|
+
- name: routine
|
|
2040
|
+
in: path
|
|
2041
|
+
required: true
|
|
2042
|
+
description: The routine's name, user@COMPUTER:script, e.g. user@HOST:Repo/folder/code (percent-encode '#')
|
|
2043
|
+
schema:
|
|
2044
|
+
type: string
|
|
2045
|
+
get:
|
|
2046
|
+
tags:
|
|
2047
|
+
- Workers
|
|
2048
|
+
summary: One routine of a schedule (master token)
|
|
2049
|
+
responses:
|
|
2050
|
+
'200':
|
|
2051
|
+
description: '{schedule, revision, routine: ScheduleRoutine}'
|
|
2052
|
+
'403':
|
|
2053
|
+
description: Master token required
|
|
2054
|
+
'404':
|
|
2055
|
+
description: No such routine in the schedule
|
|
2056
|
+
put:
|
|
2057
|
+
tags:
|
|
2058
|
+
- Workers
|
|
2059
|
+
summary: The routine becomes these fields; one left out takes its default, except args (master token)
|
|
2060
|
+
requestBody:
|
|
2061
|
+
required: true
|
|
2062
|
+
content:
|
|
2063
|
+
application/json:
|
|
2064
|
+
schema:
|
|
2065
|
+
$ref: '#/components/schemas/ScheduleRoutineEdit'
|
|
2066
|
+
responses:
|
|
2067
|
+
'200':
|
|
2068
|
+
description: '{schedule, revision, routine: ScheduleRoutine}; a new user, computer or script renames it'
|
|
2069
|
+
'400':
|
|
2070
|
+
description: A field is wrong, revision missing, or the edit breaks a dependency. Nothing was written.
|
|
2071
|
+
'403':
|
|
2072
|
+
description: Master token required
|
|
2073
|
+
'404':
|
|
2074
|
+
description: No such schedule or routine
|
|
2075
|
+
'409':
|
|
2076
|
+
description: The schedule changed since its revision was read, or the new name is taken
|
|
2077
|
+
patch:
|
|
2078
|
+
tags:
|
|
2079
|
+
- Workers
|
|
2080
|
+
summary: Only the fields sent change; the routine is checked whole afterwards (master token)
|
|
2081
|
+
requestBody:
|
|
2082
|
+
required: true
|
|
2083
|
+
content:
|
|
2084
|
+
application/json:
|
|
2085
|
+
schema:
|
|
2086
|
+
$ref: '#/components/schemas/ScheduleRoutineEdit'
|
|
2087
|
+
responses:
|
|
2088
|
+
'200':
|
|
2089
|
+
description: '{schedule, revision, routine: ScheduleRoutine}'
|
|
2090
|
+
'400':
|
|
2091
|
+
description: A field is wrong, nothing to change, revision missing, or the edit breaks a dependency
|
|
2092
|
+
'403':
|
|
2093
|
+
description: Master token required
|
|
2094
|
+
'404':
|
|
2095
|
+
description: No such schedule or routine
|
|
2096
|
+
'409':
|
|
2097
|
+
description: The schedule changed since its revision was read, or the new name is taken
|
|
2098
|
+
delete:
|
|
2099
|
+
tags:
|
|
2100
|
+
- Workers
|
|
2101
|
+
summary: Remove a routine from a schedule; its running process is not stopped (master token)
|
|
2102
|
+
parameters:
|
|
2103
|
+
- name: revision
|
|
2104
|
+
in: query
|
|
2105
|
+
description: The revision read, when it is not sent as {"revision"} in the body
|
|
2106
|
+
schema:
|
|
2107
|
+
type: string
|
|
2108
|
+
responses:
|
|
2109
|
+
'200':
|
|
2110
|
+
description: '{schedule, revision, deleted}'
|
|
2111
|
+
'400':
|
|
2112
|
+
description: Revision missing, or another routine depends on this one
|
|
2113
|
+
'403':
|
|
2114
|
+
description: Master token required
|
|
2115
|
+
'404':
|
|
2116
|
+
description: No such schedule or routine
|
|
2117
|
+
'409':
|
|
2118
|
+
description: The schedule changed since its revision was read
|
|
1877
2119
|
/api/routes:
|
|
1878
2120
|
get:
|
|
1879
2121
|
tags:
|
|
@@ -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()
|