datex-studio-cli 0.4.17__tar.gz → 0.4.18__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 (213) hide show
  1. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/CONTEXT.md +4 -0
  2. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/PKG-INFO +1 -1
  3. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/pyproject.toml +1 -1
  4. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/__init__.py +1 -1
  5. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/devops.py +227 -2
  6. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/devops/client.py +41 -0
  7. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/devops/helpers.py +56 -0
  8. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/devops/models.py +130 -0
  9. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/.gitignore +0 -0
  10. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/README.md +0 -0
  11. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/.env.example +0 -0
  12. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/Dockerfile +0 -0
  13. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/README.md +0 -0
  14. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/apply-local-cache.js +0 -0
  15. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/collector-config.yaml +0 -0
  16. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/docker-compose.yml +0 -0
  17. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/local-test-settings.json +0 -0
  18. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/remote-settings.json +0 -0
  19. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/report-creator.skill +0 -0
  20. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/scripts/analyze_corpus.py +0 -0
  21. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/scripts/generate_rdlx_schema.py +0 -0
  22. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/__main__.py +0 -0
  23. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/cli.py +0 -0
  24. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/__init__.py +0 -0
  25. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/api.py +0 -0
  26. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/auth.py +0 -0
  27. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/branch.py +0 -0
  28. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/cascade.py +0 -0
  29. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/config.py +0 -0
  30. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/configuration.py +0 -0
  31. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/crm.py +0 -0
  32. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/crm_delivery.py +0 -0
  33. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/crm_time.py +0 -0
  34. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/datasource.py +0 -0
  35. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/document.py +0 -0
  36. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/endpoint.py +0 -0
  37. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/env.py +0 -0
  38. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/explore.py +0 -0
  39. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/function.py +0 -0
  40. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/init.py +0 -0
  41. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/marketplace.py +0 -0
  42. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/__init__.py +0 -0
  43. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/_common.py +0 -0
  44. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/api.py +0 -0
  45. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/create.py +0 -0
  46. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/data.py +0 -0
  47. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/pack.py +0 -0
  48. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/preview.py +0 -0
  49. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/odata.py +0 -0
  50. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/organization.py +0 -0
  51. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/proxy.py +0 -0
  52. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/reference.py +0 -0
  53. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/release_notes.py +0 -0
  54. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/repo.py +0 -0
  55. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/__init__.py +0 -0
  56. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/add_cmds.py +0 -0
  57. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/api.py +0 -0
  58. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/batch.py +0 -0
  59. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/create.py +0 -0
  60. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/data.py +0 -0
  61. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/dataset.py +0 -0
  62. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/edit.py +0 -0
  63. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/folder.py +0 -0
  64. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/preview.py +0 -0
  65. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/schema_cmds.py +0 -0
  66. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/reset.py +0 -0
  67. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/schema.py +0 -0
  68. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/servicepack.py +0 -0
  69. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/source.py +0 -0
  70. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/studio.py +0 -0
  71. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/telemetry.py +0 -0
  72. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/user.py +0 -0
  73. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/validation_report.py +0 -0
  74. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/context.py +0 -0
  75. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/__init__.py +0 -0
  76. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/agent_browser.py +0 -0
  77. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/__init__.py +0 -0
  78. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/app_config.py +0 -0
  79. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/client.py +0 -0
  80. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/endpoints.py +0 -0
  81. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/metadata_models.py +0 -0
  82. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/models.py +0 -0
  83. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/reference_graph.py +0 -0
  84. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/__init__.py +0 -0
  85. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/datex_tenant.py +0 -0
  86. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/decorators.py +0 -0
  87. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/msal_client.py +0 -0
  88. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/tenants.py +0 -0
  89. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/token_cache.py +0 -0
  90. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/cache.py +0 -0
  91. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/__init__.py +0 -0
  92. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/flow_generator.py +0 -0
  93. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/flow_models.py +0 -0
  94. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/flow_shape.py +0 -0
  95. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/generator.py +0 -0
  96. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/models.py +0 -0
  97. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/parsers.py +0 -0
  98. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/resolver.py +0 -0
  99. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/type_def_parser.py +0 -0
  100. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/validator.py +0 -0
  101. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/designer/__init__.py +0 -0
  102. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/designer/flow_models.py +0 -0
  103. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/designer/models.py +0 -0
  104. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/designer/parsers.py +0 -0
  105. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/devops/__init__.py +0 -0
  106. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/__init__.py +0 -0
  107. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/client.py +0 -0
  108. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/html.py +0 -0
  109. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/metadata_cache.py +0 -0
  110. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/resolvers.py +0 -0
  111. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/time_entry.py +0 -0
  112. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/footprint/__init__.py +0 -0
  113. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/footprint/client.py +0 -0
  114. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/footprint/edmx_parser.py +0 -0
  115. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/footprint/metadata.py +0 -0
  116. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/function/__init__.py +0 -0
  117. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/function/generator.py +0 -0
  118. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/function/models.py +0 -0
  119. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/function/validator.py +0 -0
  120. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/graph.py +0 -0
  121. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/onboarding.py +0 -0
  122. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/__init__.py +0 -0
  123. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/csv_fmt.py +0 -0
  124. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/formatter.py +0 -0
  125. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/json_fmt.py +0 -0
  126. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/redaction.py +0 -0
  127. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/yaml_fmt.py +0 -0
  128. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/release_notes.py +0 -0
  129. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/responses.py +0 -0
  130. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/__init__.py +0 -0
  131. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/config.py +0 -0
  132. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/identity.py +0 -0
  133. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/recorder.py +0 -0
  134. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/scrubber.py +0 -0
  135. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/sender.py +0 -0
  136. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/models/__init__.py +0 -0
  137. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/__init__.py +0 -0
  138. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/folder.py +0 -0
  139. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/manifest.py +0 -0
  140. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/mocks.py +0 -0
  141. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/preview.py +0 -0
  142. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/scaffold.py +0 -0
  143. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/serve_lock.py +0 -0
  144. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/py.typed +0 -0
  145. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/__init__.py +0 -0
  146. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/datasource_binding.py +0 -0
  147. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/engine.py +0 -0
  148. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/field_parser.py +0 -0
  149. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/folder.py +0 -0
  150. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/manifest.py +0 -0
  151. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/models.py +0 -0
  152. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/preview.py +0 -0
  153. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/preview_arjs.py +0 -0
  154. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/schema.py +0 -0
  155. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/svg_renderer.py +0 -0
  156. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/templates/__init__.py +0 -0
  157. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
  158. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
  159. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/validate_arjs.py +0 -0
  160. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/validator.py +0 -0
  161. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/wrapper.py +0 -0
  162. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/__init__.py +0 -0
  163. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/click_options.py +0 -0
  164. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/clipboard.py +0 -0
  165. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/config.py +0 -0
  166. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/errors.py +0 -0
  167. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/filtering.py +0 -0
  168. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/image.py +0 -0
  169. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/paths.py +0 -0
  170. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/resolvers.py +0 -0
  171. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/responses.py +0 -0
  172. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/restricted.py +0 -0
  173. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/sorting.py +0 -0
  174. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/update_check.py +0 -0
  175. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/versioning.py +0 -0
  176. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/__init__.py +0 -0
  177. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/app.py +0 -0
  178. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/design.py +0 -0
  179. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/proxy_app.py +0 -0
  180. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/404/index.html +0 -0
  181. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/404.html +0 -0
  182. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
  183. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
  184. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
  185. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
  186. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
  187. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
  188. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
  189. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
  190. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
  191. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
  192. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
  193. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-a5e129c2d223432c.js +0 -0
  194. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/app/design/page-21ba66a3dd3b03f0.js +0 -0
  195. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
  196. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
  197. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
  198. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
  199. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
  200. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
  201. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
  202. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  203. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
  204. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
  205. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/design/capture/index.html +0 -0
  206. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/design/capture/index.txt +0 -0
  207. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/design/index.html +0 -0
  208. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/design/index.txt +0 -0
  209. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/index.html +0 -0
  210. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/index.txt +0 -0
  211. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/render-cli.js +0 -0
  212. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/report-preview.html +0 -0
  213. {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/report-validate.html +0 -0
@@ -7,3 +7,7 @@ The command-line interface (`dxs`) through which LLM agents and humans work with
7
7
  **Quartermaster**:
8
8
  The Datex-owned service that is the dxs CLI's server-side counterpart — it issues supplies (skills, context) and keeps the records (telemetry, config index).
9
9
  _Avoid_: companion service, dxs backend, dxs service
10
+
11
+ **Endpoint family**:
12
+ A cohesive group of Quartermaster endpoints serving one CLI capability (telemetry ingest, feedback, skills distribution, OData proxy, config indexing, context store). The unit in which Quartermaster is composed, enabled, and — when one earns it — deployed separately.
13
+ _Avoid_: module, microservice, subsystem
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: datex-studio-cli
3
- Version: 0.4.17
3
+ Version: 0.4.18
4
4
  Summary: CLI for Datex Studio low-code platform, designed for LLM-based AI agents
5
5
  Project-URL: Homepage, https://github.com/datex/datex-studio-cli
6
6
  Project-URL: Documentation, https://github.com/datex/datex-studio-cli
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datex-studio-cli"
7
- version = "0.4.17"
7
+ version = "0.4.18"
8
8
  description = "CLI for Datex Studio low-code platform, designed for LLM-based AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,4 +1,4 @@
1
1
  """Datex Studio CLI - Command-line interface for Datex Studio platform."""
2
2
 
3
- __version__ = "0.4.17"
3
+ __version__ = "0.4.18"
4
4
  __app_name__ = "dxs"
@@ -1,13 +1,20 @@
1
- """Azure DevOps commands: dxs devops [workitem|workitems|attachments]."""
1
+ """Azure DevOps commands: dxs devops [workitem|workitems|attachments|search|wiql]."""
2
2
 
3
3
  import pathlib
4
+ from typing import Any
4
5
 
5
6
  import click
6
7
 
7
8
  from dxs.cli import DxsContext, pass_context
8
9
  from dxs.core.auth import require_auth
9
10
  from dxs.core.devops import AzureDevOpsClient
10
- from dxs.core.devops.helpers import resolve_attachment, resolve_devops_org
11
+ from dxs.core.devops.helpers import (
12
+ chunk_ids,
13
+ resolve_attachment,
14
+ resolve_devops_org,
15
+ validate_wiql_query,
16
+ )
17
+ from dxs.core.devops.models import DevOpsWorkItemDto, WiqlResultDto
11
18
  from dxs.utils.errors import ValidationError
12
19
  from dxs.utils.responses import list_response, search_response, single
13
20
 
@@ -25,6 +32,7 @@ def devops() -> None:
25
32
  dxs devops workitems 1234,1235,1236 --org myorg
26
33
  dxs devops attachments 1234 --org myorg
27
34
  dxs devops search "login bug" --org myorg
35
+ dxs devops wiql "SELECT [System.Id],[System.Title] FROM WorkItems WHERE ..." --org myorg
28
36
  """
29
37
  pass
30
38
 
@@ -502,3 +510,220 @@ def search(
502
510
  includes_description=description,
503
511
  )
504
512
  )
513
+
514
+
515
+ # WIQL matches are hydrated one work item at a time, so the cap keeps an
516
+ # open-ended query from turning into hundreds of batch requests.
517
+ MAX_WIQL_LIMIT = 1000
518
+
519
+ # The work items batch API accepts at most 200 IDs per request.
520
+ WIQL_BATCH_SIZE = 200
521
+
522
+
523
+ def _hydrated_row(
524
+ item: DevOpsWorkItemDto,
525
+ columns: list[str],
526
+ include_id: bool = True,
527
+ ) -> dict:
528
+ """Shape one hydrated work item for WIQL output.
529
+
530
+ Uses the query's SELECT columns when the response reported any, and falls
531
+ back to the standard summary shape when it did not.
532
+ """
533
+ if columns:
534
+ return item.to_columns(columns, include_id=include_id)
535
+
536
+ summary = item.to_summary()
537
+ if not include_id:
538
+ summary.pop("id", None)
539
+ return summary
540
+
541
+
542
+ @devops.command()
543
+ @click.argument("query", required=False)
544
+ @click.option(
545
+ "-Q",
546
+ "--query-file",
547
+ type=click.Path(exists=True, dir_okay=False),
548
+ default=None,
549
+ help="Read the WIQL query from a file instead of the QUERY argument",
550
+ )
551
+ @click.option(
552
+ "--org",
553
+ "organization",
554
+ required=False,
555
+ default=None,
556
+ help="Azure DevOps organization name",
557
+ )
558
+ @click.option(
559
+ "--project",
560
+ default=None,
561
+ help="Azure DevOps project name (required for @project and project-scoped queries)",
562
+ )
563
+ @click.option(
564
+ "--team",
565
+ default=None,
566
+ help="Azure DevOps team name for team macros like @currentIteration (requires --project)",
567
+ )
568
+ @click.option(
569
+ "--limit",
570
+ "-l",
571
+ type=int,
572
+ default=200,
573
+ help=f"Maximum results to return (default: 200, max: {MAX_WIQL_LIMIT})",
574
+ )
575
+ @click.option(
576
+ "--ids-only",
577
+ is_flag=True,
578
+ default=False,
579
+ help="Return matched IDs without fetching field values",
580
+ )
581
+ @pass_context
582
+ @require_auth
583
+ def wiql(
584
+ ctx: DxsContext,
585
+ query: str | None,
586
+ query_file: str | None,
587
+ organization: str | None,
588
+ project: str | None,
589
+ team: str | None,
590
+ limit: int,
591
+ ids_only: bool,
592
+ ) -> None:
593
+ """Run an ad-hoc WIQL query.
594
+
595
+ Executes the query server-side, then fetches exactly the fields named in
596
+ its SELECT clause for each match. Both query shapes are supported:
597
+
598
+ \b
599
+ FROM WorkItems flat — one row per work item
600
+ FROM WorkItemLinks tree / one-hop — one row per source→target link,
601
+ with the target's fields merged into the row
602
+
603
+ 'metadata.total_count' is how many results the query matched;
604
+ 'metadata.count' is how many came back after --limit. 'truncated' is true
605
+ when the cap was reached and the query may have matched more.
606
+
607
+ \b
608
+ Arguments:
609
+ QUERY WIQL query text (omit when using --query-file)
610
+
611
+ \b
612
+ Options:
613
+ --org ORG Azure DevOps organization name
614
+ --project PROJ Project to scope the query to
615
+ --team TEAM Team to scope the query to (requires --project)
616
+ -Q, --query-file Read the query from a file
617
+ -l, --limit N Maximum results (default: 200)
618
+ --ids-only Return IDs only, skipping the field fetch
619
+
620
+ \b
621
+ Examples:
622
+ dxs devops wiql "SELECT [System.Id],[System.Title],[System.State] \
623
+ FROM WorkItems WHERE [System.State] = 'Active' ORDER BY [System.ChangedDate] DESC"
624
+ dxs devops wiql -Q ./active-bugs.wiql --org myorg --limit 25
625
+ dxs devops wiql "SELECT [System.Id] FROM WorkItems WHERE [System.AssignedTo] = @me" --ids-only
626
+ dxs devops wiql "SELECT [System.Id],[System.Title] FROM WorkItemLinks \
627
+ WHERE [Source].[System.Id] = 1234 AND [System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward' \
628
+ MODE (Recursive)" --project FootPrint
629
+ """
630
+ if query and query_file:
631
+ raise ValidationError(
632
+ message="Pass the query as an argument or with --query-file, not both.",
633
+ code="DXS-DEVOPS-WIQL-003",
634
+ suggestions=["Drop the QUERY argument, or drop --query-file"],
635
+ )
636
+
637
+ if not query and not query_file:
638
+ raise ValidationError(
639
+ message="A WIQL query is required.",
640
+ code="DXS-DEVOPS-WIQL-004",
641
+ suggestions=[
642
+ "dxs devops wiql \"SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'Active'\"",
643
+ "dxs devops wiql -Q query.wiql",
644
+ ],
645
+ )
646
+
647
+ if team and not project:
648
+ raise ValidationError(
649
+ message="--team also needs --project.",
650
+ code="DXS-DEVOPS-WIQL-005",
651
+ suggestions=["Teams are scoped to a project: --project <name> --team <name>"],
652
+ )
653
+
654
+ if limit < 1 or limit > MAX_WIQL_LIMIT:
655
+ raise ValidationError(
656
+ message=f"--limit must be between 1 and {MAX_WIQL_LIMIT}.",
657
+ code="DXS-DEVOPS-WIQL-006",
658
+ suggestions=["Narrow the query with a WHERE clause instead of raising the cap"],
659
+ )
660
+
661
+ raw_query = pathlib.Path(query_file).read_text(encoding="utf-8") if query_file else str(query)
662
+ wiql_query = validate_wiql_query(raw_query)
663
+
664
+ org = resolve_devops_org(organization)
665
+ ctx.log(f"Running WIQL query against {org}...")
666
+
667
+ with AzureDevOpsClient(org) as client:
668
+ result = WiqlResultDto.from_response(
669
+ client.run_wiql(wiql_query, project=project, team=team, top=limit)
670
+ )
671
+
672
+ links = result.links[:limit]
673
+ if result.is_link_query:
674
+ total_count = len(result.links)
675
+ # A tree result lists every node exactly once as a target, so
676
+ # hydrating targets covers the whole graph.
677
+ page = [link.target_id for link in links]
678
+ page = list(dict.fromkeys(page))
679
+ else:
680
+ total_count = len(result.ids)
681
+ page = result.ids[:limit]
682
+
683
+ hydrated: dict[int, DevOpsWorkItemDto] = {}
684
+ if not ids_only and page:
685
+ if total_count > limit:
686
+ ctx.log(f"{total_count} matched; fetching the first {limit}...")
687
+ fields = result.columns or None
688
+ for batch in chunk_ids(page, size=WIQL_BATCH_SIZE):
689
+ for item in client.get_workitems_batch(batch, fields=fields):
690
+ hydrated[item.id] = item
691
+
692
+ items: list[Any] = []
693
+ if result.is_link_query:
694
+ for link in links:
695
+ row: dict = {
696
+ "rel": link.rel,
697
+ "source_id": link.source_id,
698
+ "target_id": link.target_id,
699
+ }
700
+ target = hydrated.get(link.target_id)
701
+ if target is not None:
702
+ row.update(_hydrated_row(target, result.columns, include_id=False))
703
+ items.append(row)
704
+ semantic_key = "workitem_links"
705
+ elif ids_only:
706
+ items.extend(page)
707
+ semantic_key = "workitems"
708
+ else:
709
+ # The batch API does not guarantee request order; restore the WIQL one.
710
+ items.extend(_hydrated_row(hydrated[i], result.columns) for i in page if i in hydrated)
711
+ semantic_key = "workitems"
712
+
713
+ ctx.output(
714
+ search_response(
715
+ items=items,
716
+ query=wiql_query,
717
+ total_count=total_count,
718
+ semantic_key=semantic_key,
719
+ organization=org,
720
+ project=project,
721
+ team=team,
722
+ query_type=result.query_type,
723
+ columns=result.columns,
724
+ as_of=result.as_of,
725
+ limit=limit,
726
+ truncated=total_count >= limit,
727
+ ids_only=ids_only,
728
+ )
729
+ )
@@ -355,6 +355,47 @@ class AzureDevOpsClient:
355
355
  data = self._make_request("POST", url, json_data={"query": wiql})
356
356
  return [int(ref["id"]) for ref in data.get("workItems", [])]
357
357
 
358
+ def run_wiql(
359
+ self,
360
+ query: str,
361
+ project: str | None = None,
362
+ team: str | None = None,
363
+ top: int | None = None,
364
+ ) -> dict[str, Any]:
365
+ """Run an arbitrary WIQL query and return the raw response.
366
+
367
+ The API answers with references only — ``workItems`` (ids) for a flat
368
+ query, ``workItemRelations`` (source/target pairs) for a tree or
369
+ one-hop query. Parse with ``WiqlResultDto.from_response()`` and hydrate
370
+ the ids with ``get_workitems_batch()``.
371
+
372
+ Args:
373
+ query: WIQL query text.
374
+ project: Optional project to scope the query to. Required for the
375
+ ``@project`` macro and any project-scoped query.
376
+ team: Optional team to scope the query to, for team macros such as
377
+ ``@currentIteration``. Requires ``project``.
378
+ top: Server-side cap on results ($top).
379
+
380
+ Returns:
381
+ Raw WIQL response body.
382
+
383
+ Raises:
384
+ ValueError: If ``team`` is given without ``project``.
385
+ ApiError: If the request fails.
386
+ """
387
+ if team and not project:
388
+ raise ValueError("A team-scoped WIQL query also needs a project")
389
+
390
+ scope = "".join(f"/{part}" for part in (project, team) if part)
391
+ url = f"{self.base_url}{scope}/_apis/wit/wiql"
392
+
393
+ params: dict[str, Any] = {}
394
+ if top is not None:
395
+ params["$top"] = top
396
+
397
+ return self._make_request("POST", url, params=params, json_data={"query": query})
398
+
358
399
  def get_workitem_discussions(
359
400
  self,
360
401
  workitem_id: int,
@@ -74,3 +74,59 @@ def resolve_attachment(
74
74
  code="DXS-DEVOPS-ATTACH-002",
75
75
  suggestions=[f"Available attachments: {', '.join(names)}"],
76
76
  )
77
+
78
+
79
+ def validate_wiql_query(text: str) -> str:
80
+ """Validate ad-hoc WIQL text and return it stripped.
81
+
82
+ WIQL is a read-only query language, so anything that is not a SELECT is a
83
+ typo rather than a dangerous statement — it is rejected locally to save a
84
+ round trip that could only fail.
85
+
86
+ Args:
87
+ text: Raw query text from the CLI argument or --query-file.
88
+
89
+ Returns:
90
+ The query, stripped of surrounding whitespace.
91
+
92
+ Raises:
93
+ ValidationError: If the query is empty or does not start with SELECT.
94
+ """
95
+ query = text.strip()
96
+
97
+ if not query:
98
+ raise ValidationError(
99
+ message="WIQL query cannot be empty.",
100
+ code="DXS-DEVOPS-WIQL-001",
101
+ suggestions=[
102
+ "dxs devops wiql \"SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'Active'\"",
103
+ "Or pass a file: dxs devops wiql -Q query.wiql",
104
+ ],
105
+ )
106
+
107
+ if not query.upper().startswith("SELECT"):
108
+ raise ValidationError(
109
+ message="WIQL queries must start with SELECT.",
110
+ code="DXS-DEVOPS-WIQL-002",
111
+ details={"query": query[:200]},
112
+ suggestions=[
113
+ "WIQL is read-only — only SELECT statements are valid",
114
+ "Flat form: SELECT [System.Id],[System.Title] FROM WorkItems WHERE ...",
115
+ "Link form: SELECT [System.Id] FROM WorkItemLinks WHERE ... MODE (Recursive)",
116
+ ],
117
+ )
118
+
119
+ return query
120
+
121
+
122
+ def chunk_ids(ids: list[int], size: int = 200) -> list[list[int]]:
123
+ """Split work item IDs into batches the work items API will accept.
124
+
125
+ Args:
126
+ ids: Work item IDs to hydrate.
127
+ size: Maximum IDs per batch (the API caps a batch request at 200).
128
+
129
+ Returns:
130
+ List of ID batches, in order.
131
+ """
132
+ return [ids[i : i + size] for i in range(0, len(ids), size)]
@@ -91,6 +91,30 @@ def _flatten_person(value: Any) -> str:
91
91
  return str(value) if value else ""
92
92
 
93
93
 
94
+ # Reference names whose values arrive as HTML and are converted to markdown.
95
+ _HTML_FIELD_REF_NAMES = frozenset(ref for ref, _ in _HTML_FIELDS)
96
+
97
+
98
+ def format_field_value(reference_name: str, value: Any) -> Any:
99
+ """Normalize a raw work item field value for CLI output.
100
+
101
+ Identity objects collapse to their display name and known HTML fields are
102
+ converted to markdown; every other value passes through unchanged.
103
+
104
+ Args:
105
+ reference_name: Field reference name (e.g. "System.AssignedTo").
106
+ value: Raw value from the work item's fields dict.
107
+
108
+ Returns:
109
+ Value shaped for output.
110
+ """
111
+ if isinstance(value, dict) and "displayName" in value:
112
+ return _flatten_person(value)
113
+ if reference_name in _HTML_FIELD_REF_NAMES and isinstance(value, str) and value:
114
+ return _html_to_markdown(value)
115
+ return value
116
+
117
+
94
118
  class DevOpsWorkItemDto(BaseModel):
95
119
  """Azure DevOps work item information."""
96
120
 
@@ -281,6 +305,33 @@ class DevOpsWorkItemDto(BaseModel):
281
305
 
282
306
  return detail
283
307
 
308
+ def to_columns(self, columns: list[str], include_id: bool = True) -> dict[str, Any]:
309
+ """Project the work item onto an explicit column list.
310
+
311
+ Used by ad-hoc WIQL queries, where the caller's SELECT clause - not a
312
+ curated field set - decides what comes back. Columns are emitted in
313
+ SELECT order; a selected field the work item does not carry is emitted
314
+ as null so every row has the same keys.
315
+
316
+ Args:
317
+ columns: Field reference names from the WIQL SELECT clause.
318
+ "System.Id" is ignored - the id is emitted as "id".
319
+ include_id: Emit the leading "id" key. Link-query rows address the
320
+ work item as "target_id" instead, so they turn this off.
321
+
322
+ Returns:
323
+ One row: optional id, the selected columns, then the web url.
324
+ """
325
+ row: dict[str, Any] = {"id": self.id} if include_id else {}
326
+ for reference_name in columns:
327
+ if reference_name == "System.Id":
328
+ continue
329
+ row[reference_name] = format_field_value(
330
+ reference_name, self.fields.get(reference_name)
331
+ )
332
+ row["url"] = self.web_url or self.url
333
+ return row
334
+
284
335
  def to_summary(
285
336
  self, include_description: bool = False, include_discussions: bool = False
286
337
  ) -> dict[str, Any]:
@@ -324,3 +375,82 @@ class DevOpsWorkItemDto(BaseModel):
324
375
  summary["discussion_count"] = len(self.discussions)
325
376
 
326
377
  return summary
378
+
379
+
380
+ class WiqlLinkDto(BaseModel):
381
+ """One source -> target pair from a link (tree / one-hop) WIQL result."""
382
+
383
+ rel: str | None = None
384
+ source_id: int | None = None
385
+ target_id: int
386
+
387
+
388
+ class WiqlResultDto(BaseModel):
389
+ """Normalized WIQL response.
390
+
391
+ Azure DevOps answers a WIQL query with references only: a flat query
392
+ returns ``workItems`` (ids), a link query returns ``workItemRelations``
393
+ (source/target pairs). This model flattens both into ids to hydrate plus,
394
+ for link queries, the relations to report.
395
+ """
396
+
397
+ query_type: str = "flat"
398
+ columns: list[str] = Field(default_factory=list)
399
+ as_of: str | None = None
400
+ ids: list[int] = Field(default_factory=list)
401
+ links: list[WiqlLinkDto] = Field(default_factory=list)
402
+
403
+ @property
404
+ def is_link_query(self) -> bool:
405
+ """True when the query returned relations rather than a flat list."""
406
+ return bool(self.links) or self.query_type in ("tree", "oneHop")
407
+
408
+ @classmethod
409
+ def from_response(cls, data: dict[str, Any]) -> "WiqlResultDto":
410
+ """Parse a raw WIQL API response.
411
+
412
+ Args:
413
+ data: JSON body from POST _apis/wit/wiql.
414
+
415
+ Returns:
416
+ Normalized result. For link queries ``ids`` holds the distinct
417
+ target ids in response order - in a tree result every node appears
418
+ exactly once as a target, so hydrating targets covers every node.
419
+ """
420
+ columns = [
421
+ str(col["referenceName"])
422
+ for col in data.get("columns", [])
423
+ if isinstance(col, dict) and col.get("referenceName")
424
+ ]
425
+
426
+ links: list[WiqlLinkDto] = []
427
+ ids: list[int] = []
428
+
429
+ if "workItemRelations" in data:
430
+ seen: set[int] = set()
431
+ for relation in data.get("workItemRelations") or []:
432
+ target = relation.get("target") or {}
433
+ target_id = target.get("id")
434
+ if target_id is None:
435
+ continue
436
+ source = relation.get("source") or {}
437
+ links.append(
438
+ WiqlLinkDto(
439
+ rel=relation.get("rel"),
440
+ source_id=source.get("id"),
441
+ target_id=int(target_id),
442
+ )
443
+ )
444
+ if int(target_id) not in seen:
445
+ seen.add(int(target_id))
446
+ ids.append(int(target_id))
447
+ else:
448
+ ids = [int(ref["id"]) for ref in data.get("workItems") or [] if ref.get("id")]
449
+
450
+ return cls(
451
+ query_type=str(data.get("queryType", "flat")),
452
+ columns=columns,
453
+ as_of=data.get("asOf"),
454
+ ids=ids,
455
+ links=links,
456
+ )