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.
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/CONTEXT.md +4 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/PKG-INFO +1 -1
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/pyproject.toml +1 -1
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/__init__.py +1 -1
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/devops.py +227 -2
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/devops/client.py +41 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/devops/helpers.py +56 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/devops/models.py +130 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/.gitignore +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/README.md +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/.env.example +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/Dockerfile +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/README.md +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/apply-local-cache.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/collector-config.yaml +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/docker-compose.yml +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/local-test-settings.json +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/otel/remote-settings.json +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/report-creator.skill +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/scripts/analyze_corpus.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/scripts/generate_rdlx_schema.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/__main__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/cli.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/api.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/auth.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/branch.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/cascade.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/config.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/configuration.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/crm.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/crm_delivery.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/crm_time.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/datasource.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/document.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/endpoint.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/env.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/explore.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/function.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/init.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/marketplace.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/_common.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/api.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/create.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/data.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/pack.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/ng/preview.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/odata.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/organization.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/proxy.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/reference.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/release_notes.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/repo.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/add_cmds.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/api.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/batch.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/create.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/data.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/dataset.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/edit.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/folder.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/preview.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/report/schema_cmds.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/reset.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/schema.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/servicepack.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/source.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/studio.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/telemetry.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/user.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/commands/validation_report.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/context.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/agent_browser.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/app_config.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/client.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/endpoints.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/metadata_models.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/models.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/api/reference_graph.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/datex_tenant.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/decorators.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/msal_client.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/tenants.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/auth/token_cache.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/cache.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/flow_generator.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/flow_models.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/flow_shape.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/generator.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/models.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/parsers.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/resolver.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/type_def_parser.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/datasource/validator.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/designer/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/designer/flow_models.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/designer/models.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/designer/parsers.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/devops/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/client.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/html.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/metadata_cache.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/resolvers.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/dynamics/time_entry.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/footprint/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/footprint/client.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/footprint/edmx_parser.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/footprint/metadata.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/function/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/function/generator.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/function/models.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/function/validator.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/graph.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/onboarding.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/csv_fmt.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/formatter.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/json_fmt.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/redaction.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/output/yaml_fmt.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/release_notes.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/responses.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/config.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/identity.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/recorder.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/scrubber.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/core/telemetry/sender.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/models/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/folder.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/manifest.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/mocks.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/preview.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/scaffold.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/ng/serve_lock.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/py.typed +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/datasource_binding.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/engine.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/field_parser.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/folder.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/manifest.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/models.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/preview.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/preview_arjs.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/schema.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/svg_renderer.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/templates/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/validate_arjs.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/validator.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/report/wrapper.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/click_options.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/clipboard.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/config.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/errors.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/filtering.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/image.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/paths.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/resolvers.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/responses.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/restricted.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/sorting.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/update_check.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/utils/versioning.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/__init__.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/app.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/design.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/proxy_app.py +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/404/index.html +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/404.html +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
- {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
- {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
- {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
- {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
- {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
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
- {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
- {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
- {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
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/design/capture/index.html +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/design/capture/index.txt +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/design/index.html +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/design/index.txt +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/index.html +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/index.txt +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/render-cli.js +0 -0
- {datex_studio_cli-0.4.17 → datex_studio_cli-0.4.18}/src/dxs/web/static/report-preview.html +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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
|
+
)
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|