dirigent-server 0.18.4__tar.gz → 0.19.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.
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/PKG-INFO +5 -5
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/pyproject.toml +5 -5
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/pyproject.toml.orig +5 -5
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/app.py +9 -4
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/messages.py +20 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/__init__.py +14 -2
- dirigent_server-0.19.0/src/dirigent_server/routes/playground.py +483 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/LICENSE +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/README.md +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/THIRD_PARTY_NOTICES.md +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/__init__.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/dependencies.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/errors.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/health.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/logging.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/pagination.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/py.typed +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/alerts.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/auth.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/blocks.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/connections.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/examples.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/hooks.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/pipelines.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/runs.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/schema.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/schemas.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/system.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/trigger_documents.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/triggers.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/users.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/workers.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/security.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/.gitkeep +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/AdminOnly-CWzFAkRQ.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Alerting-DPHrAbP5.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Blocks-CTbBgJSs.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Breadcrumb-x6p00HyX.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/CodeEditor-B9sClhqO.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/CodeEditor-C4OnjBMt.css +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Connections-CjyNbx7P.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Dashboard-DSlMoM2G.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Description-MmXUnb-9.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ExamplePanel-Ba6aib_L.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Examples-qo66eKED.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Instant-mr1E5jAK.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/JsonBlock-CZ3pzdQd.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/KindChip-CwU_Xxdy.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ListTable-D-mI8v9Y.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Mark-DQR2php6.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Markdown-roa7jpVu.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Overview-CZJOH_Ci.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/PageState-BLuobMye.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Panel-CqrOYByl.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Picker-C5k6L204.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/PipelineEditor-B1_fPiWM.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/PipelineGraph-D4mIFvg8.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/PipelinePreview-Bh31a8YB.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/PipelineTab-Cfr2dyAj.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ProgramReference-DF37mMAT.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Refusal-B2rlk8VD.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/RunDetail-GgC-1Ma3.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/RunGraph-raonFnyd.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/SchemaForm-C08AaK2P.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Schemas-D5uR8yla.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Segmented-KaDiGiyr.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/SettingsDialog-CgALkqHY.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/StarterPicker-CcaWNru5.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/StepPorts-PlS_VS4B.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/TileCard-DERgB2kj.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Triggers-D5cdnjJV.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Users-DYxSHyt1.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/WindowedPane-BUe4ZBNS.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Workers-Bb39s8gk.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/WorkersTable-Bdjj57jO.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/api-Bm1JZLuC.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/auth-CHgfcdt9.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/badge-BovCxjd1.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/blocks-CclbVd8c.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/chevron-down-DAO3SUWv.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/chunk-OB3PAWPO-DOp4K52_.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/codicon-DCmgc-ay.ttf +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/connections-DsdoSARb.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/copy-BeBwg8IR.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/dist-DD8kpv1W.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/docs-CCoB3shg.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/dropdown-menu-B-6CcHKw.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/editor.worker-CIvRzd4O.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/elk-engine-Bm7I_NRI.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/elk-worker.min-DfmSo98M.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/examples-DDWkSZ2u.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/file-braces-Ep8AP5Yq.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/format-B5YbYOY-.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/glyphs-C1sjljkh.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-600-normal-CTOM6hUh.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-600-normal-fLZuRloM.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-ext-600-normal-9HEixskS.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-cyrillic-ext-600-normal-V-xxqcpd.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-ext-600-normal-D38SheWl.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-latin-ext-600-normal-DmB0ttJJ.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-vietnamese-600-normal-D2EvbN8M.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-mono-vietnamese-600-normal-iLQfcSjf.woff +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/identity-BNZxCf_y.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/idle-DnO3aHK7.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/index-Bp4KzK9w.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/index-CgkcXl1G.css +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/input-1fShkFXn.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/json.worker-fczZDBja.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/jsonMode-C2rhmYxE.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/nav-i7bJ0lrA.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/paging-CpRIkEky.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/pipelines-Pi3WL8R6.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/play-CPVSEWx6.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/preferences-CEibwN5K.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/preload-helper-BZ1Pz5am.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/react-CQOZoUwg.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/resolveAriaLabelledBy-ClHvyYLK.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/rolldown-runtime-Dd_uD5pT.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/schemas-7KY_7aIc.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/select-Dl8ZhXSH.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/shell-CC2rA5mh.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/sql-NEE52Syq.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/starters-CgC7I45V.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/status-Ctx8x8Mf.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/store-C2zgoyrT.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/textarea-ljIbN_md.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/tooltip-D7_KwbOU.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/triggers-BetQ4Leu.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/twig-De2hgUGE.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/use-heartbeat-CBT8848s.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/use-store-CfRquqZF.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/users-CTGHiHMA.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/utils-CT7ZGLfR.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/with-selector-BXYKoaOE.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/workers-BH2Za19E.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/x-Dltw6Vbs.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/yaml-CLSOANGG.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/yaml.worker-B2SaT_wA.js +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/favicon-32.png +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/index.html +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/logo.png +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/transactions.py +0 -0
- {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/ui.py +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirigent-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: Dirigent FastAPI server: the REST API, its authentication, and the health probes.
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
License-File: THIRD_PARTY_NOTICES.md
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
-
Requires-Dist: dirigent-client==0.
|
|
11
|
-
Requires-Dist: dirigent-common==0.
|
|
12
|
-
Requires-Dist: dirigent-core==0.
|
|
13
|
-
Requires-Dist: dirigent-examples==0.
|
|
10
|
+
Requires-Dist: dirigent-client==0.19.0
|
|
11
|
+
Requires-Dist: dirigent-common==0.19.0
|
|
12
|
+
Requires-Dist: dirigent-core==0.19.0
|
|
13
|
+
Requires-Dist: dirigent-examples==0.19.0
|
|
14
14
|
Requires-Dist: fastapi>=0.141.1
|
|
15
15
|
Requires-Python: >=3.13
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-server"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.19.0"
|
|
4
4
|
description = "Dirigent FastAPI server: the REST API, its authentication, and the health probes."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -14,10 +14,10 @@ classifiers = [
|
|
|
14
14
|
"Programming Language :: Python :: 3.13",
|
|
15
15
|
]
|
|
16
16
|
dependencies = [
|
|
17
|
-
"dirigent-client==0.
|
|
18
|
-
"dirigent-common==0.
|
|
19
|
-
"dirigent-core==0.
|
|
20
|
-
"dirigent-examples==0.
|
|
17
|
+
"dirigent-client==0.19.0",
|
|
18
|
+
"dirigent-common==0.19.0",
|
|
19
|
+
"dirigent-core==0.19.0",
|
|
20
|
+
"dirigent-examples==0.19.0",
|
|
21
21
|
"fastapi>=0.141.1",
|
|
22
22
|
]
|
|
23
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-server"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.19.0"
|
|
4
4
|
description = "Dirigent FastAPI server: the REST API, its authentication, and the health probes."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -11,10 +11,10 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"dirigent-client==0.
|
|
15
|
-
"dirigent-common==0.
|
|
16
|
-
"dirigent-core==0.
|
|
17
|
-
"dirigent-examples==0.
|
|
14
|
+
"dirigent-client==0.19.0",
|
|
15
|
+
"dirigent-common==0.19.0",
|
|
16
|
+
"dirigent-core==0.19.0",
|
|
17
|
+
"dirigent-examples==0.19.0",
|
|
18
18
|
"fastapi>=0.141.1",
|
|
19
19
|
]
|
|
20
20
|
|
|
@@ -29,9 +29,14 @@ A generic pipeline orchestrator. Pipelines are data, composed from pluggable bui
|
|
|
29
29
|
blocks and executed as a DAG on a durable engine.
|
|
30
30
|
|
|
31
31
|
Every endpoint under `/api/v1` requires authentication: a bearer token for automation, or
|
|
32
|
-
the session cookie `POST /api/v1/auth/login` sets for a browser. The probes under `/health
|
|
33
|
-
|
|
34
|
-
versioned API and authenticates with its own
|
|
32
|
+
the session cookie `POST /api/v1/auth/login` sets for a browser. The probes under `/health`,
|
|
33
|
+
this document itself, and the playground under `/api/v1/playground` are the exceptions.
|
|
34
|
+
`POST /hooks/{token}` is outside the versioned API and authenticates with its own
|
|
35
|
+
per-trigger token.
|
|
36
|
+
|
|
37
|
+
The playground serves generated data and chosen behaviour to documents that need something
|
|
38
|
+
to happen: it needs no credential, opens no database session, and reads no instance data.
|
|
39
|
+
`DIRIGENT_PLAYGROUND_ENABLED=false` unmounts it.
|
|
35
40
|
"""
|
|
36
41
|
|
|
37
42
|
_logger = get_logger("server")
|
|
@@ -99,7 +104,7 @@ def create_app(
|
|
|
99
104
|
mount_ui_assets(app, resolved)
|
|
100
105
|
app.include_router(health_router)
|
|
101
106
|
app.include_router(build_hooks_router())
|
|
102
|
-
app.include_router(build_router(), prefix=resolved.api_prefix)
|
|
107
|
+
app.include_router(build_router(resolved), prefix=resolved.api_prefix)
|
|
103
108
|
mount_ui_shell(app, resolved)
|
|
104
109
|
instrument_fastapi(app)
|
|
105
110
|
return app
|
|
@@ -97,3 +97,23 @@ TOO_MANY_TAILS = SERVER.define("too_many_tails", "you already have {maximum} str
|
|
|
97
97
|
NO_ICON = SERVER.define("no_icon", "this bundle carries no icon")
|
|
98
98
|
|
|
99
99
|
BAD_CURSOR = SERVER.define("bad_cursor", "{name}={after} is not a cursor this listing gave out")
|
|
100
|
+
|
|
101
|
+
PLAYGROUND_BAD_KNOB = SERVER.define("playground_bad_knob", "{detail}")
|
|
102
|
+
|
|
103
|
+
PLAYGROUND_HEADER_REFUSED = SERVER.define(
|
|
104
|
+
"playground_header_refused",
|
|
105
|
+
"the playground will not set {header} on itself: a header that plants a cookie, opens "
|
|
106
|
+
"this origin to another site, or weakens what a browser enforces here teaches nothing",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
PLAYGROUND_OFF_INSTANCE = SERVER.define(
|
|
110
|
+
"playground_off_instance",
|
|
111
|
+
"{to} is not a path on this instance, and the playground redirects nowhere else: an "
|
|
112
|
+
"open redirect is a phishing tool wearing this instance's own domain",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
PLAYGROUND_UNAUTHENTICATED = SERVER.define(
|
|
116
|
+
"playground_unauthenticated",
|
|
117
|
+
"this route wants a credential: the basic pair {username}/{username}, or the documented "
|
|
118
|
+
"bearer token. Both are public constants and guard nothing",
|
|
119
|
+
)
|
|
@@ -7,6 +7,7 @@ rather than as a person, so it is mounted at the application root instead.
|
|
|
7
7
|
|
|
8
8
|
from fastapi import APIRouter, Depends
|
|
9
9
|
|
|
10
|
+
from dirigent_core.config import Settings
|
|
10
11
|
from dirigent_server.routes import (
|
|
11
12
|
alerts,
|
|
12
13
|
auth,
|
|
@@ -15,6 +16,7 @@ from dirigent_server.routes import (
|
|
|
15
16
|
examples,
|
|
16
17
|
hooks,
|
|
17
18
|
pipelines,
|
|
19
|
+
playground,
|
|
18
20
|
runs,
|
|
19
21
|
schema,
|
|
20
22
|
schemas,
|
|
@@ -43,13 +45,23 @@ TAGS: list[dict[str, str]] = [
|
|
|
43
45
|
{"name": "users", "description": "Local accounts."},
|
|
44
46
|
{"name": "system", "description": "What this instance is, and whether it is well."},
|
|
45
47
|
{"name": "health", "description": "Liveness and readiness."},
|
|
48
|
+
{
|
|
49
|
+
"name": "playground",
|
|
50
|
+
"description": "Generated data and chosen behaviour, for examples and tests. No credential.",
|
|
51
|
+
},
|
|
46
52
|
]
|
|
47
53
|
|
|
48
54
|
|
|
49
|
-
def build_router() -> APIRouter:
|
|
50
|
-
"""Assemble the versioned API: the
|
|
55
|
+
def build_router(settings: Settings | None = None) -> APIRouter:
|
|
56
|
+
"""Assemble the versioned API: the open routes, then everything behind authentication.
|
|
57
|
+
|
|
58
|
+
The playground is mounted only when the instance serves it, so turning it off is not a
|
|
59
|
+
route that refuses but a route that is not there.
|
|
60
|
+
"""
|
|
51
61
|
router = APIRouter(route_class=Transactional)
|
|
52
62
|
router.include_router(auth.public_router)
|
|
63
|
+
if settings is None or settings.playground_enabled:
|
|
64
|
+
router.include_router(playground.router)
|
|
53
65
|
guarded = APIRouter(dependencies=[Depends(require_principal)], route_class=Transactional)
|
|
54
66
|
for module in (
|
|
55
67
|
auth,
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""The playground: a request-and-response service the instance serves itself.
|
|
2
|
+
|
|
3
|
+
Unauthenticated, because a pipeline step calling its own instance's playground must not need
|
|
4
|
+
a token, and because there is nothing here to protect: no database session is opened, no
|
|
5
|
+
instance data is read, and nothing is remembered between requests.
|
|
6
|
+
|
|
7
|
+
Every route answers the same envelope. ``kind`` names the answer, ``request`` holds what
|
|
8
|
+
arrived verbatim, and every other key is what the route decided -- the knobs resolved, after
|
|
9
|
+
defaults and after a seed was drawn. So ``request.args.rows`` is the string that was sent
|
|
10
|
+
and ``rows`` is the number that was used.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import base64
|
|
15
|
+
import binascii
|
|
16
|
+
import json
|
|
17
|
+
import secrets
|
|
18
|
+
from datetime import timedelta
|
|
19
|
+
from typing import Annotated, Final
|
|
20
|
+
from urllib.parse import unquote_plus, urlencode
|
|
21
|
+
|
|
22
|
+
from fastapi import APIRouter, Query, Request, Response
|
|
23
|
+
from fastapi import status as http_status
|
|
24
|
+
from fastapi.responses import JSONResponse
|
|
25
|
+
from pydantic import BaseModel, JsonValue
|
|
26
|
+
|
|
27
|
+
from dirigent_common import Duration
|
|
28
|
+
from dirigent_server.errors import Refusal
|
|
29
|
+
from dirigent_server.messages import (
|
|
30
|
+
PLAYGROUND_BAD_KNOB,
|
|
31
|
+
PLAYGROUND_HEADER_REFUSED,
|
|
32
|
+
PLAYGROUND_OFF_INSTANCE,
|
|
33
|
+
PLAYGROUND_UNAUTHENTICATED,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
router = APIRouter(tags=["playground"])
|
|
37
|
+
|
|
38
|
+
PREFIX: Final = "/playground"
|
|
39
|
+
|
|
40
|
+
#: Reflected as this rather than repeated, so a page on this origin cannot read a session
|
|
41
|
+
#: cookie back out of the playground that ``httponly`` exists to keep from it.
|
|
42
|
+
REDACTED: Final = "<redacted>"
|
|
43
|
+
|
|
44
|
+
#: Request headers whose value is never reflected, only their presence.
|
|
45
|
+
SECRET_HEADERS: Final = frozenset({"authorization", "cookie", "proxy-authorization"})
|
|
46
|
+
|
|
47
|
+
#: Statuses that carry no body, so the reflection has nowhere to go.
|
|
48
|
+
BODYLESS: Final = frozenset({204, 304})
|
|
49
|
+
|
|
50
|
+
#: The redirect statuses the playground will issue. A 304 is not a redirect and carries no
|
|
51
|
+
#: body, and no other 3xx is one a client follows.
|
|
52
|
+
REDIRECTS: Final = frozenset({301, 302, 303, 307, 308})
|
|
53
|
+
|
|
54
|
+
#: The longest redirect chain one call may ask for.
|
|
55
|
+
MAX_HOPS: Final = 10
|
|
56
|
+
|
|
57
|
+
#: The longest a call may be asked to take before answering. The same ceiling the
|
|
58
|
+
#: ``playground.generate`` node puts on its own ``delay``.
|
|
59
|
+
MAX_DELAY: Final = timedelta(seconds=30)
|
|
60
|
+
|
|
61
|
+
#: Response headers the playground will not set on itself, because each one would make this
|
|
62
|
+
#: origin do something for a caller that is not teaching anything: plant a cookie, open the
|
|
63
|
+
#: origin to another site, or weaken what a browser enforces here.
|
|
64
|
+
FORBIDDEN_HEADERS: Final = frozenset(
|
|
65
|
+
{
|
|
66
|
+
"connection",
|
|
67
|
+
"content-length",
|
|
68
|
+
"content-security-policy",
|
|
69
|
+
"set-cookie",
|
|
70
|
+
"set-cookie2",
|
|
71
|
+
"strict-transport-security",
|
|
72
|
+
"transfer-encoding",
|
|
73
|
+
"upgrade",
|
|
74
|
+
"x-frame-options",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
#: Query names the routes read as knobs, so ``response-headers`` never sets one as a header.
|
|
79
|
+
RESERVED_ARGS: Final = frozenset({"delay", "status"})
|
|
80
|
+
|
|
81
|
+
#: The credential ``/playground/auth`` accepts. Public constants, documented as such: they
|
|
82
|
+
#: protect nothing and unlock nothing but this one route's 200.
|
|
83
|
+
BASIC_USERNAME: Final = "playground"
|
|
84
|
+
BASIC_PASSWORD: Final = "playground"
|
|
85
|
+
BEARER_TOKEN: Final = "playground-token"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class RequestFacts(BaseModel):
|
|
89
|
+
"""What arrived, verbatim: the same shape on every playground answer."""
|
|
90
|
+
|
|
91
|
+
method: str
|
|
92
|
+
"""The HTTP method the call was made with."""
|
|
93
|
+
|
|
94
|
+
url: str
|
|
95
|
+
"""The whole URL as this instance saw it, query string included."""
|
|
96
|
+
|
|
97
|
+
path: str
|
|
98
|
+
"""The path alone, without the query."""
|
|
99
|
+
|
|
100
|
+
args: dict[str, JsonValue]
|
|
101
|
+
"""The query arguments as strings, with a repeated name arriving as a list."""
|
|
102
|
+
|
|
103
|
+
headers: dict[str, str]
|
|
104
|
+
"""The request headers, lowercased. A credential's presence is shown, never its value."""
|
|
105
|
+
|
|
106
|
+
body: JsonValue = None
|
|
107
|
+
"""The body, decoded the way ``body_kind`` says. Null when there was none."""
|
|
108
|
+
|
|
109
|
+
body_kind: str = "none"
|
|
110
|
+
"""How the body was read: ``none``, ``json``, ``form`` or ``text``. A body appears once."""
|
|
111
|
+
|
|
112
|
+
body_bytes: int = 0
|
|
113
|
+
"""How large the body was, in bytes."""
|
|
114
|
+
|
|
115
|
+
content_type: str | None = None
|
|
116
|
+
"""The content type the body arrived under, as the header spelled it."""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Answer(BaseModel):
|
|
120
|
+
"""The envelope every playground route answers with."""
|
|
121
|
+
|
|
122
|
+
kind: str
|
|
123
|
+
"""What this answer is, which is what a reader dispatches on."""
|
|
124
|
+
|
|
125
|
+
request: RequestFacts
|
|
126
|
+
"""What the call arrived as."""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Reflection(Answer):
|
|
130
|
+
"""A call reflected back, with nothing added."""
|
|
131
|
+
|
|
132
|
+
status: int
|
|
133
|
+
"""The status this answer was given."""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Redirect(Answer):
|
|
137
|
+
"""One hop of a redirect chain."""
|
|
138
|
+
|
|
139
|
+
status: int
|
|
140
|
+
"""The redirect status this hop answered with."""
|
|
141
|
+
|
|
142
|
+
location: str
|
|
143
|
+
"""Where this hop points, which is the next hop or the destination."""
|
|
144
|
+
|
|
145
|
+
hops: int
|
|
146
|
+
"""How many hops remain after this one."""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Unreliable(Answer):
|
|
150
|
+
"""A call that fails while the attempt it names is inside the failing window."""
|
|
151
|
+
|
|
152
|
+
status: int
|
|
153
|
+
"""The status this answer was given."""
|
|
154
|
+
|
|
155
|
+
attempt: int
|
|
156
|
+
"""Which attempt the caller said this is."""
|
|
157
|
+
|
|
158
|
+
fail_until: int
|
|
159
|
+
"""How many attempts fail before one succeeds."""
|
|
160
|
+
|
|
161
|
+
outcome: str
|
|
162
|
+
"""``failed`` while the window lasts, then ``succeeded``."""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class SetHeaders(Answer):
|
|
166
|
+
"""The headers the playground set on the way back."""
|
|
167
|
+
|
|
168
|
+
status: int
|
|
169
|
+
"""The status this answer was given."""
|
|
170
|
+
|
|
171
|
+
headers: dict[str, str]
|
|
172
|
+
"""Every header this answer set, exactly as it was asked to."""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Credential(Answer):
|
|
176
|
+
"""A call that presented a credential the playground accepts."""
|
|
177
|
+
|
|
178
|
+
status: int
|
|
179
|
+
"""The status this answer was given."""
|
|
180
|
+
|
|
181
|
+
authenticated: bool
|
|
182
|
+
"""Always true: a call that was not authenticated is refused rather than answered."""
|
|
183
|
+
|
|
184
|
+
scheme: str
|
|
185
|
+
"""Which HTTP authentication scheme the accepted credential used."""
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _args(request: Request) -> dict[str, JsonValue]:
|
|
189
|
+
"""The query string as the playground reports it: one value a string, a repeated one a list."""
|
|
190
|
+
gathered: dict[str, list[str]] = {}
|
|
191
|
+
for name, value in request.query_params.multi_items():
|
|
192
|
+
gathered.setdefault(name, []).append(value)
|
|
193
|
+
return {name: values[0] if len(values) == 1 else list(values) for name, values in gathered.items()}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _headers(request: Request) -> dict[str, str]:
|
|
197
|
+
"""The request headers, lowercased, with every credential's value replaced."""
|
|
198
|
+
return {
|
|
199
|
+
name.lower(): REDACTED if name.lower() in SECRET_HEADERS else value for name, value in request.headers.items()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _body(raw: bytes, content_type: str | None) -> tuple[JsonValue, str]:
|
|
204
|
+
"""Decode a body once, and say how it was decoded."""
|
|
205
|
+
if not raw:
|
|
206
|
+
return None, "none"
|
|
207
|
+
media = (content_type or "").split(";")[0].strip().lower()
|
|
208
|
+
text = raw.decode("utf-8", errors="replace")
|
|
209
|
+
if media == "application/json":
|
|
210
|
+
try:
|
|
211
|
+
return json.loads(text), "json"
|
|
212
|
+
except json.JSONDecodeError:
|
|
213
|
+
return text, "text"
|
|
214
|
+
if media == "application/x-www-form-urlencoded":
|
|
215
|
+
gathered: dict[str, JsonValue] = {}
|
|
216
|
+
for pair in text.split("&"):
|
|
217
|
+
if not pair:
|
|
218
|
+
continue
|
|
219
|
+
name, _, value = pair.partition("=")
|
|
220
|
+
gathered[unquote_plus(name)] = unquote_plus(value)
|
|
221
|
+
return gathered, "form"
|
|
222
|
+
return text, "text"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def _facts(request: Request) -> RequestFacts:
|
|
226
|
+
"""Gather what arrived, for the envelope every route shares."""
|
|
227
|
+
raw = await request.body()
|
|
228
|
+
content_type = request.headers.get("content-type")
|
|
229
|
+
body, body_kind = _body(raw, content_type)
|
|
230
|
+
return RequestFacts(
|
|
231
|
+
method=request.method,
|
|
232
|
+
url=str(request.url),
|
|
233
|
+
path=request.url.path,
|
|
234
|
+
args=_args(request),
|
|
235
|
+
headers=_headers(request),
|
|
236
|
+
body=body,
|
|
237
|
+
body_kind=body_kind,
|
|
238
|
+
body_bytes=len(raw),
|
|
239
|
+
content_type=content_type,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _answer(model: BaseModel, code: int, headers: dict[str, str] | None = None) -> Response:
|
|
244
|
+
"""Send one answer, leaving out the body where the status cannot carry one."""
|
|
245
|
+
if code in BODYLESS or code < 200:
|
|
246
|
+
return Response(status_code=code, headers=headers)
|
|
247
|
+
return JSONResponse(model.model_dump(mode="json"), status_code=code, headers=headers)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _bad_knob(detail: str) -> Refusal:
|
|
251
|
+
"""Refuse a knob the playground could not honour, naming what was wrong with it."""
|
|
252
|
+
return Refusal(PLAYGROUND_BAD_KNOB, status=http_status.HTTP_422_UNPROCESSABLE_CONTENT, detail=detail)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def _delayed(delay: timedelta) -> None:
|
|
256
|
+
"""Take as long as the delay asks for, refusing one longer than the playground allows."""
|
|
257
|
+
if delay < timedelta(0) or delay > MAX_DELAY:
|
|
258
|
+
raise _bad_knob(f"a delay is between 0 and {int(MAX_DELAY.total_seconds())}s")
|
|
259
|
+
if delay > timedelta(0):
|
|
260
|
+
await asyncio.sleep(delay.total_seconds())
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
StatusArg = Annotated[int, Query(ge=100, le=599, description="The status to answer with.")]
|
|
264
|
+
DelayArg = Annotated[Duration, Query(description="How long to take before answering, as `250ms` or `2s`.")]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@router.api_route(
|
|
268
|
+
f"{PREFIX}/request",
|
|
269
|
+
methods=["GET"],
|
|
270
|
+
operation_id="playgroundRequestGet",
|
|
271
|
+
summary="Reflect a GET back",
|
|
272
|
+
response_model=Reflection,
|
|
273
|
+
)
|
|
274
|
+
@router.api_route(
|
|
275
|
+
f"{PREFIX}/request",
|
|
276
|
+
methods=["POST"],
|
|
277
|
+
operation_id="playgroundRequestPost",
|
|
278
|
+
summary="Reflect a POST and its body back",
|
|
279
|
+
response_model=Reflection,
|
|
280
|
+
)
|
|
281
|
+
@router.api_route(
|
|
282
|
+
f"{PREFIX}/request",
|
|
283
|
+
methods=["PUT"],
|
|
284
|
+
operation_id="playgroundRequestPut",
|
|
285
|
+
summary="Reflect a PUT and its body back",
|
|
286
|
+
response_model=Reflection,
|
|
287
|
+
)
|
|
288
|
+
@router.api_route(
|
|
289
|
+
f"{PREFIX}/request",
|
|
290
|
+
methods=["PATCH"],
|
|
291
|
+
operation_id="playgroundRequestPatch",
|
|
292
|
+
summary="Reflect a PATCH and its body back",
|
|
293
|
+
response_model=Reflection,
|
|
294
|
+
)
|
|
295
|
+
@router.api_route(
|
|
296
|
+
f"{PREFIX}/request",
|
|
297
|
+
methods=["DELETE"],
|
|
298
|
+
operation_id="playgroundRequestDelete",
|
|
299
|
+
summary="Reflect a DELETE back",
|
|
300
|
+
response_model=Reflection,
|
|
301
|
+
)
|
|
302
|
+
async def reflect(request: Request, status: StatusArg = 200, delay: DelayArg = timedelta(0)) -> Response:
|
|
303
|
+
"""Answer with what arrived, under the status that was asked for.
|
|
304
|
+
|
|
305
|
+
The degenerate route: every other one reflects too, and adds an answer of its own.
|
|
306
|
+
"""
|
|
307
|
+
await _delayed(delay)
|
|
308
|
+
return _answer(Reflection(kind="request", request=await _facts(request), status=status), status)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@router.get(
|
|
312
|
+
f"{PREFIX}/redirect",
|
|
313
|
+
operation_id="playgroundRedirect",
|
|
314
|
+
summary="Issue a redirect, or a chain of them",
|
|
315
|
+
response_model=Redirect,
|
|
316
|
+
status_code=http_status.HTTP_302_FOUND,
|
|
317
|
+
responses={422: {"description": "A status that is not a redirect, or a destination off this instance."}},
|
|
318
|
+
)
|
|
319
|
+
async def redirect(
|
|
320
|
+
request: Request,
|
|
321
|
+
hops: Annotated[int, Query(ge=1, le=MAX_HOPS, description="How many redirects to issue before arriving.")] = 1,
|
|
322
|
+
to: Annotated[
|
|
323
|
+
str | None,
|
|
324
|
+
Query(description="Where the last hop points, as a path on this instance. Defaults to the reflection route."),
|
|
325
|
+
] = None,
|
|
326
|
+
status: Annotated[int, Query(description="The redirect status each hop answers with.")] = 302,
|
|
327
|
+
delay: DelayArg = timedelta(0),
|
|
328
|
+
) -> Response:
|
|
329
|
+
"""Point at the next hop, or at the destination when this is the last one.
|
|
330
|
+
|
|
331
|
+
The destination is a path on this instance and never an absolute URL: an open redirect
|
|
332
|
+
is a phishing tool wearing the instance's own domain, and there is nothing to teach
|
|
333
|
+
that needs one.
|
|
334
|
+
"""
|
|
335
|
+
if status not in REDIRECTS:
|
|
336
|
+
raise _bad_knob(f"{status} is not a redirect status; use one of {sorted(REDIRECTS)}")
|
|
337
|
+
destination = _destination(request, to)
|
|
338
|
+
await _delayed(delay)
|
|
339
|
+
location = destination if hops <= 1 else _next_hop(request, hops - 1, to, status)
|
|
340
|
+
answer = Redirect(
|
|
341
|
+
kind="redirect",
|
|
342
|
+
request=await _facts(request),
|
|
343
|
+
status=status,
|
|
344
|
+
location=location,
|
|
345
|
+
hops=hops - 1,
|
|
346
|
+
)
|
|
347
|
+
return _answer(answer, status, headers={"location": location})
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _destination(request: Request, to: str | None) -> str:
|
|
351
|
+
"""Where the chain ends: a path on this instance, defaulting to the reflection route."""
|
|
352
|
+
if to is None:
|
|
353
|
+
return f"{request.url.path.removesuffix('/redirect')}/request"
|
|
354
|
+
if not to.startswith("/") or to.startswith("//"):
|
|
355
|
+
raise Refusal(
|
|
356
|
+
PLAYGROUND_OFF_INSTANCE,
|
|
357
|
+
status=http_status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
358
|
+
to=to,
|
|
359
|
+
)
|
|
360
|
+
return to
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _next_hop(request: Request, hops: int, to: str | None, status: int) -> str:
|
|
364
|
+
"""The same route again with one fewer hop, so a chain walks itself down to the destination."""
|
|
365
|
+
query = {"hops": str(hops), "status": str(status)}
|
|
366
|
+
if to is not None:
|
|
367
|
+
query["to"] = to
|
|
368
|
+
return f"{request.url.path}?{urlencode(query)}"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@router.get(
|
|
372
|
+
f"{PREFIX}/unreliable",
|
|
373
|
+
operation_id="playgroundUnreliable",
|
|
374
|
+
summary="Fail until a given attempt, then succeed",
|
|
375
|
+
response_model=Unreliable,
|
|
376
|
+
)
|
|
377
|
+
async def unreliable(
|
|
378
|
+
request: Request,
|
|
379
|
+
attempt: Annotated[int, Query(ge=1, description="Which attempt the caller says this is.")] = 1,
|
|
380
|
+
fail_until: Annotated[int, Query(ge=0, description="How many attempts fail before one succeeds.")] = 1,
|
|
381
|
+
status: Annotated[int, Query(ge=100, le=599, description="The status answered while failing.")] = 503,
|
|
382
|
+
delay: DelayArg = timedelta(0),
|
|
383
|
+
) -> Response:
|
|
384
|
+
"""Answer the failing status while the attempt is inside the window, and 200 after it.
|
|
385
|
+
|
|
386
|
+
The window is driven by the attempt the caller names, because the playground remembers
|
|
387
|
+
nothing between requests and an answer that depended on what it remembered could not be
|
|
388
|
+
reproduced.
|
|
389
|
+
"""
|
|
390
|
+
await _delayed(delay)
|
|
391
|
+
failing = attempt <= fail_until
|
|
392
|
+
code = status if failing else http_status.HTTP_200_OK
|
|
393
|
+
answer = Unreliable(
|
|
394
|
+
kind="unreliable",
|
|
395
|
+
request=await _facts(request),
|
|
396
|
+
status=code,
|
|
397
|
+
attempt=attempt,
|
|
398
|
+
fail_until=fail_until,
|
|
399
|
+
outcome="failed" if failing else "succeeded",
|
|
400
|
+
)
|
|
401
|
+
return _answer(answer, code)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@router.get(
|
|
405
|
+
f"{PREFIX}/response-headers",
|
|
406
|
+
operation_id="playgroundResponseHeaders",
|
|
407
|
+
summary="Set the query's pairs as response headers",
|
|
408
|
+
response_model=SetHeaders,
|
|
409
|
+
responses={422: {"description": "A header this origin will not set on itself."}},
|
|
410
|
+
)
|
|
411
|
+
async def response_headers(request: Request, status: StatusArg = 200, delay: DelayArg = timedelta(0)) -> Response:
|
|
412
|
+
"""Set each query argument as a response header, so a client's header handling has something to read."""
|
|
413
|
+
await _delayed(delay)
|
|
414
|
+
chosen: dict[str, str] = {}
|
|
415
|
+
for name, value in request.query_params.multi_items():
|
|
416
|
+
if name in RESERVED_ARGS:
|
|
417
|
+
continue
|
|
418
|
+
if name.lower() in FORBIDDEN_HEADERS:
|
|
419
|
+
raise Refusal(
|
|
420
|
+
PLAYGROUND_HEADER_REFUSED,
|
|
421
|
+
status=http_status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
422
|
+
header=name,
|
|
423
|
+
)
|
|
424
|
+
chosen[name] = value
|
|
425
|
+
answer = SetHeaders(kind="headers", request=await _facts(request), status=status, headers=chosen)
|
|
426
|
+
return _answer(answer, status, headers=chosen)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@router.get(
|
|
430
|
+
f"{PREFIX}/auth",
|
|
431
|
+
operation_id="playgroundAuth",
|
|
432
|
+
summary="Require a credential the playground documents",
|
|
433
|
+
response_model=Credential,
|
|
434
|
+
responses={401: {"description": "No credential, or one the playground does not accept."}},
|
|
435
|
+
)
|
|
436
|
+
async def auth(request: Request, status: StatusArg = 200, delay: DelayArg = timedelta(0)) -> Response:
|
|
437
|
+
"""Accept the documented basic pair or the documented bearer token, and refuse anything else.
|
|
438
|
+
|
|
439
|
+
The credential is a public constant. It guards nothing: it exists so a document can be
|
|
440
|
+
seen presenting one, and so the reflection can be seen redacting it.
|
|
441
|
+
"""
|
|
442
|
+
await _delayed(delay)
|
|
443
|
+
scheme = _scheme(request.headers.get("authorization"))
|
|
444
|
+
if scheme is None:
|
|
445
|
+
raise Refusal(
|
|
446
|
+
PLAYGROUND_UNAUTHENTICATED,
|
|
447
|
+
status=http_status.HTTP_401_UNAUTHORIZED,
|
|
448
|
+
headers={"WWW-Authenticate": 'Basic realm="playground"'},
|
|
449
|
+
username=BASIC_USERNAME,
|
|
450
|
+
)
|
|
451
|
+
answer = Credential(
|
|
452
|
+
kind="auth",
|
|
453
|
+
request=await _facts(request),
|
|
454
|
+
status=status,
|
|
455
|
+
authenticated=True,
|
|
456
|
+
scheme=scheme,
|
|
457
|
+
)
|
|
458
|
+
return _answer(answer, status)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _scheme(header: str | None) -> str | None:
|
|
462
|
+
"""Which scheme an acceptable credential used, or None when there was not one."""
|
|
463
|
+
if not header:
|
|
464
|
+
return None
|
|
465
|
+
kind, _, presented = header.partition(" ")
|
|
466
|
+
lowered = kind.lower()
|
|
467
|
+
if lowered == "bearer" and secrets.compare_digest(presented.strip(), BEARER_TOKEN):
|
|
468
|
+
return "bearer"
|
|
469
|
+
if lowered == "basic" and _basic(presented.strip()):
|
|
470
|
+
return "basic"
|
|
471
|
+
return None
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _basic(presented: str) -> bool:
|
|
475
|
+
"""Whether a base64 basic credential is the documented pair."""
|
|
476
|
+
try:
|
|
477
|
+
decoded = base64.b64decode(presented, validate=True).decode("utf-8")
|
|
478
|
+
except (binascii.Error, UnicodeDecodeError, ValueError):
|
|
479
|
+
return False
|
|
480
|
+
return secrets.compare_digest(decoded, f"{BASIC_USERNAME}:{BASIC_PASSWORD}")
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
__all__ = ["PREFIX", "router"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|