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.
Files changed (171) hide show
  1. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/PKG-INFO +5 -5
  2. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/pyproject.toml +5 -5
  3. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/pyproject.toml.orig +5 -5
  4. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/app.py +9 -4
  5. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/messages.py +20 -0
  6. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/__init__.py +14 -2
  7. dirigent_server-0.19.0/src/dirigent_server/routes/playground.py +483 -0
  8. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/LICENSE +0 -0
  9. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/README.md +0 -0
  10. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/THIRD_PARTY_NOTICES.md +0 -0
  11. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/__init__.py +0 -0
  12. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/dependencies.py +0 -0
  13. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/errors.py +0 -0
  14. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/health.py +0 -0
  15. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/logging.py +0 -0
  16. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/pagination.py +0 -0
  17. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/py.typed +0 -0
  18. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/alerts.py +0 -0
  19. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/auth.py +0 -0
  20. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/blocks.py +0 -0
  21. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/connections.py +0 -0
  22. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/examples.py +0 -0
  23. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/hooks.py +0 -0
  24. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/pipelines.py +0 -0
  25. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/runs.py +0 -0
  26. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/schema.py +0 -0
  27. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/schemas.py +0 -0
  28. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/system.py +0 -0
  29. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/trigger_documents.py +0 -0
  30. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/triggers.py +0 -0
  31. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/users.py +0 -0
  32. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/routes/workers.py +0 -0
  33. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/security.py +0 -0
  34. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/.gitkeep +0 -0
  35. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/AdminOnly-CWzFAkRQ.js +0 -0
  36. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Alerting-DPHrAbP5.js +0 -0
  37. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Blocks-CTbBgJSs.js +0 -0
  38. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Breadcrumb-x6p00HyX.js +0 -0
  39. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/CodeEditor-B9sClhqO.js +0 -0
  40. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/CodeEditor-C4OnjBMt.css +0 -0
  41. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Connections-CjyNbx7P.js +0 -0
  42. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Dashboard-DSlMoM2G.js +0 -0
  43. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Description-MmXUnb-9.js +0 -0
  44. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ExamplePanel-Ba6aib_L.js +0 -0
  45. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Examples-qo66eKED.js +0 -0
  46. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Instant-mr1E5jAK.js +0 -0
  47. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/JsonBlock-CZ3pzdQd.js +0 -0
  48. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/KindChip-CwU_Xxdy.js +0 -0
  49. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ListTable-D-mI8v9Y.js +0 -0
  50. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Mark-DQR2php6.js +0 -0
  51. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Markdown-roa7jpVu.js +0 -0
  52. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Overview-CZJOH_Ci.js +0 -0
  53. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/PageState-BLuobMye.js +0 -0
  54. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Panel-CqrOYByl.js +0 -0
  55. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Picker-C5k6L204.js +0 -0
  56. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/PipelineEditor-B1_fPiWM.js +0 -0
  57. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/PipelineGraph-D4mIFvg8.js +0 -0
  58. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/PipelinePreview-Bh31a8YB.js +0 -0
  59. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/PipelineTab-Cfr2dyAj.js +0 -0
  60. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/ProgramReference-DF37mMAT.js +0 -0
  61. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Refusal-B2rlk8VD.js +0 -0
  62. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/RunDetail-GgC-1Ma3.js +0 -0
  63. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/RunGraph-raonFnyd.js +0 -0
  64. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/SchemaForm-C08AaK2P.js +0 -0
  65. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Schemas-D5uR8yla.js +0 -0
  66. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Segmented-KaDiGiyr.js +0 -0
  67. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/SettingsDialog-CgALkqHY.js +0 -0
  68. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/StarterPicker-CcaWNru5.js +0 -0
  69. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/StepPorts-PlS_VS4B.js +0 -0
  70. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/TileCard-DERgB2kj.js +0 -0
  71. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Triggers-D5cdnjJV.js +0 -0
  72. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Users-DYxSHyt1.js +0 -0
  73. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/WindowedPane-BUe4ZBNS.js +0 -0
  74. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/Workers-Bb39s8gk.js +0 -0
  75. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/WorkersTable-Bdjj57jO.js +0 -0
  76. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/api-Bm1JZLuC.js +0 -0
  77. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/auth-CHgfcdt9.js +0 -0
  78. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/badge-BovCxjd1.js +0 -0
  79. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/blocks-CclbVd8c.js +0 -0
  80. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/chevron-down-DAO3SUWv.js +0 -0
  81. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/chunk-OB3PAWPO-DOp4K52_.js +0 -0
  82. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/codicon-DCmgc-ay.ttf +0 -0
  83. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/connections-DsdoSARb.js +0 -0
  84. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/copy-BeBwg8IR.js +0 -0
  85. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/dist-DD8kpv1W.js +0 -0
  86. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/docs-CCoB3shg.js +0 -0
  87. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/dropdown-menu-B-6CcHKw.js +0 -0
  88. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/editor.worker-CIvRzd4O.js +0 -0
  89. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/elk-engine-Bm7I_NRI.js +0 -0
  90. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/elk-worker.min-DfmSo98M.js +0 -0
  91. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/examples-DDWkSZ2u.js +0 -0
  92. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/file-braces-Ep8AP5Yq.js +0 -0
  93. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/format-B5YbYOY-.js +0 -0
  94. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/glyphs-C1sjljkh.js +0 -0
  95. {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
  96. {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
  97. {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
  98. {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
  99. {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
  100. {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
  101. {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
  102. {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
  103. {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
  104. {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
  105. {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
  106. {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
  107. {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
  108. {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
  109. {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
  110. {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
  111. {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
  112. {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
  113. {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
  114. {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
  115. {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
  116. {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
  117. {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
  118. {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
  119. {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
  120. {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
  121. {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
  122. {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
  123. {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
  124. {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
  125. {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
  126. {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
  127. {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
  128. {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
  129. {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
  130. {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
  131. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/identity-BNZxCf_y.js +0 -0
  132. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/idle-DnO3aHK7.js +0 -0
  133. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/index-Bp4KzK9w.js +0 -0
  134. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/index-CgkcXl1G.css +0 -0
  135. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/input-1fShkFXn.js +0 -0
  136. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/json.worker-fczZDBja.js +0 -0
  137. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/jsonMode-C2rhmYxE.js +0 -0
  138. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/nav-i7bJ0lrA.js +0 -0
  139. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/paging-CpRIkEky.js +0 -0
  140. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/pipelines-Pi3WL8R6.js +0 -0
  141. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/play-CPVSEWx6.js +0 -0
  142. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/preferences-CEibwN5K.js +0 -0
  143. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/preload-helper-BZ1Pz5am.js +0 -0
  144. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/react-CQOZoUwg.js +0 -0
  145. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/resolveAriaLabelledBy-ClHvyYLK.js +0 -0
  146. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/rolldown-runtime-Dd_uD5pT.js +0 -0
  147. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/schemas-7KY_7aIc.js +0 -0
  148. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/select-Dl8ZhXSH.js +0 -0
  149. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/shell-CC2rA5mh.js +0 -0
  150. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/sql-NEE52Syq.js +0 -0
  151. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/starters-CgC7I45V.js +0 -0
  152. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/status-Ctx8x8Mf.js +0 -0
  153. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/store-C2zgoyrT.js +0 -0
  154. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/textarea-ljIbN_md.js +0 -0
  155. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/tooltip-D7_KwbOU.js +0 -0
  156. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/triggers-BetQ4Leu.js +0 -0
  157. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/twig-De2hgUGE.js +0 -0
  158. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/use-heartbeat-CBT8848s.js +0 -0
  159. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/use-store-CfRquqZF.js +0 -0
  160. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/users-CTGHiHMA.js +0 -0
  161. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/utils-CT7ZGLfR.js +0 -0
  162. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/with-selector-BXYKoaOE.js +0 -0
  163. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/workers-BH2Za19E.js +0 -0
  164. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/x-Dltw6Vbs.js +0 -0
  165. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/yaml-CLSOANGG.js +0 -0
  166. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/assets/yaml.worker-B2SaT_wA.js +0 -0
  167. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/favicon-32.png +0 -0
  168. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/index.html +0 -0
  169. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/static/logo.png +0 -0
  170. {dirigent_server-0.18.4 → dirigent_server-0.19.0}/src/dirigent_server/transactions.py +0 -0
  171. {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.18.4
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.18.4
11
- Requires-Dist: dirigent-common==0.18.4
12
- Requires-Dist: dirigent-core==0.18.4
13
- Requires-Dist: dirigent-examples==0.18.4
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.18.4"
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.4",
18
- "dirigent-common==0.18.4",
19
- "dirigent-core==0.18.4",
20
- "dirigent-examples==0.18.4",
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.18.4"
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.18.4",
15
- "dirigent-common==0.18.4",
16
- "dirigent-core==0.18.4",
17
- "dirigent-examples==0.18.4",
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
- and this document itself are the only exceptions. `POST /hooks/{token}` is outside the
34
- versioned API and authenticates with its own per-trigger token.
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 login route, then everything behind authentication."""
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"]