datex-studio-cli 0.4.19__tar.gz → 0.4.21__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.19 → datex_studio_cli-0.4.21}/PKG-INFO +1 -1
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/pyproject.toml +1 -1
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/__init__.py +1 -1
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/cli.py +110 -10
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/auth.py +228 -39
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/explore.py +1 -1
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/init.py +21 -16
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/source.py +10 -3
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/client.py +23 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/datex_tenant.py +209 -6
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/msal_client.py +127 -26
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/devops/client.py +2 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/client.py +5 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/config.py +16 -14
- datex_studio_cli-0.4.21/src/dxs/utils/update_check.py +452 -0
- datex_studio_cli-0.4.21/src/dxs/utils/versioning.py +22 -0
- datex_studio_cli-0.4.19/src/dxs/utils/update_check.py +0 -208
- datex_studio_cli-0.4.19/src/dxs/utils/versioning.py +0 -22
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/.gitignore +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/CONTEXT.md +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/README.md +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/.env.example +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/Dockerfile +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/README.md +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/apply-local-cache.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/collector-config.yaml +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/docker-compose.yml +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/local-test-settings.json +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/otel/remote-settings.json +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/report-creator.skill +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/scripts/analyze_corpus.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/scripts/generate_rdlx_schema.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/__main__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/api.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/branch.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/branch_commit_guard.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/cascade.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/config.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/configuration.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/crm.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/crm_delivery.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/crm_time.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/datasource.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/devops.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/document.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/endpoint.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/env.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/function.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/marketplace.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/_common.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/api.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/create.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/data.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/pack.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/ng/preview.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/odata.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/organization.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/proxy.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/reference.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/release_notes.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/repo.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/add_cmds.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/api.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/batch.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/create.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/data.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/dataset.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/edit.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/folder.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/preview.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/report/schema_cmds.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/reset.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/schema.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/servicepack.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/studio.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/telemetry.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/user.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/commands/validation_report.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/context.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/agent_browser.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/app_config.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/endpoints.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/metadata_models.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/models.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/api/reference_graph.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/decorators.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/tenants.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/auth/token_cache.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/cache.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/flow_generator.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/flow_models.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/flow_shape.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/generator.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/models.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/parsers.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/resolver.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/type_def_parser.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/datasource/validator.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/designer/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/designer/flow_models.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/designer/models.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/designer/parsers.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/devops/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/devops/helpers.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/devops/models.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/html.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/metadata_cache.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/pagination.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/resolvers.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/dynamics/time_entry.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/client.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/edmx_parser.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/footprint/metadata.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/function/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/function/generator.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/function/models.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/function/validator.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/graph.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/onboarding.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/csv_fmt.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/formatter.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/json_fmt.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/redaction.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/output/yaml_fmt.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/release_notes.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/responses.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/config.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/identity.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/recorder.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/scrubber.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/core/telemetry/sender.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/models/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/folder.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/manifest.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/mocks.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/preview.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/scaffold.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/ng/serve_lock.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/py.typed +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/datasource_binding.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/engine.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/field_parser.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/folder.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/manifest.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/models.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/preview.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/preview_arjs.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/schema.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/svg_renderer.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/templates/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/validate_arjs.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/validator.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/report/wrapper.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/click_options.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/clipboard.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/errors.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/filtering.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/image.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/paths.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/resolvers.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/responses.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/restricted.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/utils/sorting.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/__init__.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/app.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/design.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/proxy_app.py +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/404/index.html +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/404.html +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-a5e129c2d223432c.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/design/page-21ba66a3dd3b03f0.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/capture/index.html +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/capture/index.txt +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/index.html +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/design/index.txt +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/index.html +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/index.txt +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/render-cli.js +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/report-preview.html +0 -0
- {datex_studio_cli-0.4.19 → datex_studio_cli-0.4.21}/src/dxs/web/static/report-validate.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: datex-studio-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.21
|
|
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
|
|
@@ -70,6 +70,17 @@ def _stdout_is_pipe() -> bool:
|
|
|
70
70
|
return False
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def _stdin_is_tty() -> bool:
|
|
74
|
+
"""Whether stdin is interactive — gates confirmation prompts.
|
|
75
|
+
|
|
76
|
+
A helper for the same reason as ``_stdout_is_pipe``: tests patch it.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
return sys.stdin.isatty()
|
|
80
|
+
except (AttributeError, ValueError):
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
73
84
|
def _ensure_utf8_streams() -> None:
|
|
74
85
|
"""Force stdout/stderr to UTF-8 so non-ASCII output never crashes the CLI.
|
|
75
86
|
|
|
@@ -571,7 +582,7 @@ def register_commands() -> None:
|
|
|
571
582
|
@click.pass_context
|
|
572
583
|
def _after_command(ctx: click.Context, result: Any, **kwargs: Any) -> Any:
|
|
573
584
|
"""Run post-command hooks (telemetry success event + PyPI update notice)."""
|
|
574
|
-
from dxs.utils.update_check import maybe_show_update_notice
|
|
585
|
+
from dxs.utils.update_check import maybe_show_update_notice, maybe_show_update_result
|
|
575
586
|
|
|
576
587
|
dxs_ctx: DxsContext | None = ctx.find_object(DxsContext)
|
|
577
588
|
|
|
@@ -585,6 +596,13 @@ def _after_command(ctx: click.Context, result: Any, **kwargs: Any) -> Any:
|
|
|
585
596
|
except Exception: # noqa: BLE001
|
|
586
597
|
pass
|
|
587
598
|
|
|
599
|
+
# The background-upgrade outcome is the result of an explicit user
|
|
600
|
+
# action, so it is not gated by --no-update-check.
|
|
601
|
+
try:
|
|
602
|
+
maybe_show_update_result()
|
|
603
|
+
except Exception: # noqa: BLE001 - never let the update check break the CLI
|
|
604
|
+
pass
|
|
605
|
+
|
|
588
606
|
skip = bool(dxs_ctx and dxs_ctx.no_update_check)
|
|
589
607
|
try:
|
|
590
608
|
maybe_show_update_notice(skip=skip)
|
|
@@ -606,13 +624,23 @@ def _ctx_extras(dxs_ctx: "DxsContext | None") -> dict[str, Any]:
|
|
|
606
624
|
|
|
607
625
|
|
|
608
626
|
@cli.command("update")
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
627
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
|
|
628
|
+
@click.option(
|
|
629
|
+
"--check",
|
|
630
|
+
"check_only",
|
|
631
|
+
is_flag=True,
|
|
632
|
+
help="Only check for a newer release and print the upgrade command.",
|
|
633
|
+
)
|
|
634
|
+
def update_cmd(yes: bool, check_only: bool) -> None:
|
|
635
|
+
"""Upgrade dxs to the latest release on PyPI.
|
|
636
|
+
|
|
637
|
+
On macOS/Linux this process is replaced by the installer's upgrade
|
|
638
|
+
command (uv/pipx/pip), so its output appears right here. On Windows
|
|
639
|
+
the running dxs.exe locks its own files, so the upgrade runs in a
|
|
640
|
+
detached background helper after dxs exits — the outcome is reported
|
|
641
|
+
on the next dxs run, with details in ~/.datex/update.log.
|
|
615
642
|
"""
|
|
643
|
+
import shutil
|
|
616
644
|
import time
|
|
617
645
|
|
|
618
646
|
from dxs.utils.update_check import (
|
|
@@ -620,8 +648,14 @@ def update_cmd() -> None:
|
|
|
620
648
|
_fetch_latest_version,
|
|
621
649
|
_read_cache,
|
|
622
650
|
_write_cache,
|
|
651
|
+
count_windows_dxs_processes,
|
|
652
|
+
detect_upgrade_argv,
|
|
623
653
|
detect_upgrade_command,
|
|
654
|
+
is_dev_install,
|
|
624
655
|
is_newer,
|
|
656
|
+
is_windows,
|
|
657
|
+
perform_upgrade_posix,
|
|
658
|
+
perform_upgrade_windows,
|
|
625
659
|
)
|
|
626
660
|
|
|
627
661
|
click.echo(f"Current version: {__version__}", err=True)
|
|
@@ -636,11 +670,8 @@ def update_cmd() -> None:
|
|
|
636
670
|
click.echo("You are on the latest version.", err=True)
|
|
637
671
|
return
|
|
638
672
|
|
|
639
|
-
upgrade_cmd = detect_upgrade_command()
|
|
640
673
|
click.echo("", err=True)
|
|
641
674
|
click.echo(f" A new release of dxs is available: {__version__} → {latest}", err=True)
|
|
642
|
-
click.echo(f" To upgrade, run: {upgrade_cmd}", err=True)
|
|
643
|
-
click.echo(f" https://pypi.org/project/{PACKAGE_NAME}/{latest}/", err=True)
|
|
644
675
|
|
|
645
676
|
# Refresh the cache and suppress the auto-banner for the next dedupe
|
|
646
677
|
# window — the user has explicitly seen the news.
|
|
@@ -651,6 +682,75 @@ def update_cmd() -> None:
|
|
|
651
682
|
cache["last_notified_at"] = now
|
|
652
683
|
_write_cache(cache)
|
|
653
684
|
|
|
685
|
+
if check_only:
|
|
686
|
+
click.echo(f" To upgrade, run: {detect_upgrade_command()}", err=True)
|
|
687
|
+
click.echo(f" https://pypi.org/project/{PACKAGE_NAME}/{latest}/", err=True)
|
|
688
|
+
return
|
|
689
|
+
|
|
690
|
+
if is_dev_install():
|
|
691
|
+
click.echo(
|
|
692
|
+
"This is a development install (source checkout / editable). "
|
|
693
|
+
"Upgrade it with: git pull && uv sync",
|
|
694
|
+
err=True,
|
|
695
|
+
)
|
|
696
|
+
sys.exit(1)
|
|
697
|
+
|
|
698
|
+
argv = detect_upgrade_argv()
|
|
699
|
+
resolved = shutil.which(argv[0])
|
|
700
|
+
if resolved is None:
|
|
701
|
+
click.echo(
|
|
702
|
+
f"Cannot find '{argv[0]}' on PATH — install it or upgrade manually with: "
|
|
703
|
+
f"{detect_upgrade_command()}",
|
|
704
|
+
err=True,
|
|
705
|
+
)
|
|
706
|
+
sys.exit(1)
|
|
707
|
+
argv = [resolved, *argv[1:]]
|
|
708
|
+
|
|
709
|
+
if not yes:
|
|
710
|
+
if not _stdin_is_tty():
|
|
711
|
+
click.echo(
|
|
712
|
+
"Not an interactive session — re-run with --yes to upgrade.",
|
|
713
|
+
err=True,
|
|
714
|
+
)
|
|
715
|
+
sys.exit(1)
|
|
716
|
+
click.confirm(f" Upgrade to {latest} now?", abort=True, err=True)
|
|
717
|
+
|
|
718
|
+
if is_windows():
|
|
719
|
+
running = count_windows_dxs_processes()
|
|
720
|
+
if running is not None and running > 1:
|
|
721
|
+
click.echo(
|
|
722
|
+
f" Warning: {running} dxs processes are running (dxs studio, another "
|
|
723
|
+
"terminal?). The upgrade may fail until they all exit.",
|
|
724
|
+
err=True,
|
|
725
|
+
)
|
|
726
|
+
try:
|
|
727
|
+
log_path = perform_upgrade_windows(argv, latest)
|
|
728
|
+
except OSError as e:
|
|
729
|
+
click.echo(f"Failed to start the background upgrade: {e}", err=True)
|
|
730
|
+
sys.exit(1)
|
|
731
|
+
click.echo(" The upgrade will run in the background once dxs exits.", err=True)
|
|
732
|
+
click.echo(f" Log: {log_path}", err=True)
|
|
733
|
+
click.echo(
|
|
734
|
+
" The outcome will be reported the next time you run dxs — give it a minute.",
|
|
735
|
+
err=True,
|
|
736
|
+
)
|
|
737
|
+
return
|
|
738
|
+
|
|
739
|
+
click.echo(f" Running: {' '.join(argv)}", err=True)
|
|
740
|
+
# execvp never returns on success, so the result_callback (telemetry
|
|
741
|
+
# success event) won't run — record it here first.
|
|
742
|
+
try:
|
|
743
|
+
from dxs.core.telemetry import record_end
|
|
744
|
+
|
|
745
|
+
record_end(exit_code=0, ctx_extras={})
|
|
746
|
+
except Exception: # noqa: BLE001
|
|
747
|
+
pass
|
|
748
|
+
try:
|
|
749
|
+
perform_upgrade_posix(argv)
|
|
750
|
+
except OSError as e:
|
|
751
|
+
click.echo(f"Upgrade failed to start: {e}", err=True)
|
|
752
|
+
sys.exit(1)
|
|
753
|
+
|
|
654
754
|
|
|
655
755
|
# Register commands when module loads
|
|
656
756
|
register_commands()
|
|
@@ -15,6 +15,7 @@ from dxs.core.auth.datex_tenant import (
|
|
|
15
15
|
is_datex_identity,
|
|
16
16
|
)
|
|
17
17
|
from dxs.core.auth.decorators import get_access_token
|
|
18
|
+
from dxs.core.auth.msal_client import ConsentOutcome
|
|
18
19
|
from dxs.core.auth.tenants import (
|
|
19
20
|
identity_needs_tenant_authority,
|
|
20
21
|
is_foreign_tenant,
|
|
@@ -51,6 +52,117 @@ def _reject_tenant_override(ctx: DxsContext, command: str) -> None:
|
|
|
51
52
|
raise SystemExit(1)
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
def consent_failure_items(outcomes: list[ConsentOutcome] | None) -> list[dict[str, Any]]:
|
|
56
|
+
"""Map failed consent rounds to payload entries (empty when all succeeded).
|
|
57
|
+
|
|
58
|
+
AADSTS650057 means the resource is not listed on the CLI app registration —
|
|
59
|
+
no amount of retrying or user consent can fix that, so it carries an
|
|
60
|
+
explicit escalation suggestion (issue #135).
|
|
61
|
+
"""
|
|
62
|
+
items: list[dict[str, Any]] = []
|
|
63
|
+
for outcome in outcomes or []:
|
|
64
|
+
if outcome.ok:
|
|
65
|
+
continue
|
|
66
|
+
entry: dict[str, Any] = {
|
|
67
|
+
"resource": outcome.resource,
|
|
68
|
+
"error": outcome.error,
|
|
69
|
+
"error_description": outcome.error_description,
|
|
70
|
+
}
|
|
71
|
+
if outcome.aadsts_code == "650057":
|
|
72
|
+
entry["suggestion"] = (
|
|
73
|
+
"This resource is not listed on the CLI app registration, so consent "
|
|
74
|
+
"cannot be requested for it — ask an Entra admin to grant tenant-wide "
|
|
75
|
+
"consent for the resource to the Datex Studio CLI app."
|
|
76
|
+
)
|
|
77
|
+
items.append(entry)
|
|
78
|
+
return items
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _run_targeted_consent(ctx: DxsContext, resource_key: str) -> None:
|
|
82
|
+
"""(Re)consent one Datex-internal resource without a full re-login.
|
|
83
|
+
|
|
84
|
+
Backs ``dxs auth login --consent devops|crm`` — the recovery path for a
|
|
85
|
+
user whose login-time consent round was skipped or silently failed
|
|
86
|
+
(issue #136). Uses the cached Datex identity; the consent round requests
|
|
87
|
+
the *named* scope so dynamic user consent applies (issue #135).
|
|
88
|
+
"""
|
|
89
|
+
from dxs.core.auth.datex_tenant import consent_datex_resource
|
|
90
|
+
from dxs.utils.config import get_settings
|
|
91
|
+
|
|
92
|
+
settings = get_settings()
|
|
93
|
+
if resource_key == "devops":
|
|
94
|
+
resource = "Azure DevOps"
|
|
95
|
+
consent_scopes = settings.azure_devops_scopes
|
|
96
|
+
acquire_scopes = settings.azure_devops_scopes
|
|
97
|
+
error_code = "DXS-DEVOPS-AUTH-001"
|
|
98
|
+
else:
|
|
99
|
+
if not settings.dynamics_crm_url:
|
|
100
|
+
ctx.output_error(
|
|
101
|
+
ValidationError(
|
|
102
|
+
message=(
|
|
103
|
+
"dynamics_crm_url is not configured; there is no Dynamics CRM "
|
|
104
|
+
"resource to consent to."
|
|
105
|
+
),
|
|
106
|
+
code="DXS-DYNAMICS-CONFIG-001",
|
|
107
|
+
suggestions=[
|
|
108
|
+
"Run: dxs config set dynamics_crm_url https://yourorg.crm.dynamics.com",
|
|
109
|
+
"Then re-run: dxs auth login --consent crm",
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
raise SystemExit(1)
|
|
114
|
+
resource = "Dynamics CRM"
|
|
115
|
+
consent_scopes = settings.dynamics_crm_consent_scopes
|
|
116
|
+
acquire_scopes = settings.dynamics_crm_scopes
|
|
117
|
+
error_code = "DXS-DYNAMICS-AUTH-001"
|
|
118
|
+
|
|
119
|
+
def on_device_code(device_code: dict[str, Any]) -> None:
|
|
120
|
+
ctx.output(
|
|
121
|
+
single(
|
|
122
|
+
item={
|
|
123
|
+
"status": "awaiting_authentication",
|
|
124
|
+
"action_required": {
|
|
125
|
+
"type": "device_code",
|
|
126
|
+
"instructions": (
|
|
127
|
+
f"Sign in with your Datex account to consent to {resource}"
|
|
128
|
+
),
|
|
129
|
+
"url": device_code["verification_uri"],
|
|
130
|
+
"code": device_code["user_code"],
|
|
131
|
+
"expires_in_seconds": device_code["expires_in"],
|
|
132
|
+
},
|
|
133
|
+
"message": device_code.get("message", ""),
|
|
134
|
+
},
|
|
135
|
+
semantic_key="authentication",
|
|
136
|
+
),
|
|
137
|
+
include_metadata=False,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
outcome = consent_datex_resource(
|
|
142
|
+
consent_scopes=consent_scopes,
|
|
143
|
+
acquire_scopes=acquire_scopes,
|
|
144
|
+
resource=resource,
|
|
145
|
+
error_code=error_code,
|
|
146
|
+
on_device_code=on_device_code,
|
|
147
|
+
)
|
|
148
|
+
except DxsError as e:
|
|
149
|
+
ctx.output_error(e)
|
|
150
|
+
raise SystemExit(1) from None
|
|
151
|
+
|
|
152
|
+
ctx.output(
|
|
153
|
+
single(
|
|
154
|
+
item={
|
|
155
|
+
"status": "consented",
|
|
156
|
+
"message": f"Consent for {resource} granted and verified.",
|
|
157
|
+
"resource": resource,
|
|
158
|
+
"account": outcome["identity"],
|
|
159
|
+
"has_token": True,
|
|
160
|
+
},
|
|
161
|
+
semantic_key="authentication",
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
54
166
|
def run_standard_device_login(
|
|
55
167
|
ctx: DxsContext,
|
|
56
168
|
on_device_code: Callable[[dict[str, Any]], None],
|
|
@@ -64,6 +176,7 @@ def run_standard_device_login(
|
|
|
64
176
|
organization_id: int | None = None,
|
|
65
177
|
organization_name: str | None = None,
|
|
66
178
|
api_base_url: str | None = None,
|
|
179
|
+
consent_outcomes: list[ConsentOutcome] | None = None,
|
|
67
180
|
) -> CachedIdentity:
|
|
68
181
|
"""Run the multi-resource device-code login and save the identity.
|
|
69
182
|
|
|
@@ -88,6 +201,11 @@ def run_standard_device_login(
|
|
|
88
201
|
that target a non-default environment (e.g. ``dxs init --target qa``) must
|
|
89
202
|
pass it so the identity is stamped from the same environment its profile is
|
|
90
203
|
detected against, not the stale lru_cached default.
|
|
204
|
+
|
|
205
|
+
``consent_outcomes``, when given, receives one entry per additional consent
|
|
206
|
+
round so callers can include failures in their success payload. Failed
|
|
207
|
+
rounds are always logged as warnings here regardless — an additional
|
|
208
|
+
resource failing never aborts the login, so this is the only signal.
|
|
91
209
|
"""
|
|
92
210
|
from dxs.utils.config import get_settings
|
|
93
211
|
|
|
@@ -117,25 +235,36 @@ def run_standard_device_login(
|
|
|
117
235
|
tenant is known, so DevOps/CRM consent is requested only for Datex
|
|
118
236
|
staff — a customer must never be prompted for Datex-internal
|
|
119
237
|
resources by name.
|
|
238
|
+
|
|
239
|
+
The CRM round requests the *named* scope, not ``.default``:
|
|
240
|
+
``.default`` is static consent and fails with AADSTS650057 for any
|
|
241
|
+
first-time user because Dataverse is not listed on the CLI app
|
|
242
|
+
registration (issue #135). Token acquisition keeps ``.default``.
|
|
120
243
|
"""
|
|
121
244
|
internal = (
|
|
122
245
|
[
|
|
123
246
|
settings.azure_devops_scopes,
|
|
124
|
-
settings.
|
|
125
|
-
settings.dynamics_crm_admin_scopes,
|
|
126
|
-
settings.data_export_service_scopes,
|
|
247
|
+
settings.dynamics_crm_consent_scopes,
|
|
127
248
|
]
|
|
128
249
|
if claims_are_datex(token_response.get("id_token_claims") or {})
|
|
129
250
|
else []
|
|
130
251
|
)
|
|
131
252
|
return [scopes for scopes in [*internal, [settings.footprint_scope]] if scopes]
|
|
132
253
|
|
|
254
|
+
outcomes: list[ConsentOutcome] = consent_outcomes if consent_outcomes is not None else []
|
|
133
255
|
result = client.authenticate_device_code_multi_resource(
|
|
134
256
|
primary_scopes=settings.azure_scopes,
|
|
135
257
|
additional_scope_sets=additional_scope_sets,
|
|
136
258
|
on_device_code=on_device_code,
|
|
137
259
|
on_additional_consent_needed=consent_cb,
|
|
260
|
+
consent_outcomes=outcomes,
|
|
138
261
|
)
|
|
262
|
+
for entry in consent_failure_items(outcomes):
|
|
263
|
+
detail = entry["error_description"] or entry["error"] or "unknown error"
|
|
264
|
+
warning = f"WARNING: consent for {entry['resource']} was not obtained: {detail}"
|
|
265
|
+
if "suggestion" in entry:
|
|
266
|
+
warning += f" {entry['suggestion']}"
|
|
267
|
+
ctx.log(warning)
|
|
139
268
|
|
|
140
269
|
saved_identity = cache.save_identity(
|
|
141
270
|
result,
|
|
@@ -173,13 +302,16 @@ def _auth_success_item(
|
|
|
173
302
|
saved_identity: CachedIdentity,
|
|
174
303
|
*,
|
|
175
304
|
message: str = "Successfully authenticated",
|
|
305
|
+
consent_outcomes: list[ConsentOutcome] | None = None,
|
|
176
306
|
) -> dict[str, Any]:
|
|
177
307
|
"""Build the standard ``authenticated`` response payload shared by the login paths.
|
|
178
308
|
|
|
179
309
|
The external-tenant descriptor is derived from the saved identity — never from
|
|
180
310
|
which CLI flags were passed — so the payload cannot disagree with the cache
|
|
181
311
|
(a --tenant-id login with the home tenant is persisted as internal). Adds the
|
|
182
|
-
``footprint_daemon_auth`` marker when the identity carries daemon creds
|
|
312
|
+
``footprint_daemon_auth`` marker when the identity carries daemon creds, and
|
|
313
|
+
``consent_failures`` when an additional consent round failed — the primary
|
|
314
|
+
login succeeded, so this is where a missing consent becomes visible.
|
|
183
315
|
"""
|
|
184
316
|
item: dict[str, Any] = {
|
|
185
317
|
"status": "authenticated",
|
|
@@ -194,6 +326,9 @@ def _auth_success_item(
|
|
|
194
326
|
item["expires_at"] = saved_identity.expires_at.isoformat()
|
|
195
327
|
if saved_identity.footprint_client_id:
|
|
196
328
|
item["footprint_daemon_auth"] = "configured"
|
|
329
|
+
failures = consent_failure_items(consent_outcomes)
|
|
330
|
+
if failures:
|
|
331
|
+
item["consent_failures"] = failures
|
|
197
332
|
return item
|
|
198
333
|
|
|
199
334
|
|
|
@@ -279,6 +414,16 @@ def auth() -> None:
|
|
|
279
414
|
default=None,
|
|
280
415
|
help="Entra domain (e.g. csw-wi.com) — resolves the tenant ID for you",
|
|
281
416
|
)
|
|
417
|
+
@click.option(
|
|
418
|
+
"--consent",
|
|
419
|
+
"consent_resource",
|
|
420
|
+
type=click.Choice(["devops", "crm"]),
|
|
421
|
+
default=None,
|
|
422
|
+
help=(
|
|
423
|
+
"Only (re)run the consent round for one Datex-internal resource, using the "
|
|
424
|
+
"cached Datex identity — no full re-login"
|
|
425
|
+
),
|
|
426
|
+
)
|
|
282
427
|
@pass_context
|
|
283
428
|
def login(
|
|
284
429
|
ctx: DxsContext,
|
|
@@ -287,6 +432,7 @@ def login(
|
|
|
287
432
|
client_secret: str | None,
|
|
288
433
|
tenant_id: str | None,
|
|
289
434
|
domain: str | None,
|
|
435
|
+
consent_resource: str | None,
|
|
290
436
|
) -> None:
|
|
291
437
|
"""Authenticate with Azure Entra using device code flow.
|
|
292
438
|
|
|
@@ -307,6 +453,9 @@ def login(
|
|
|
307
453
|
credentials) authentication for the Footprint API, needed for cross-tenant
|
|
308
454
|
access where OBO flow is not available.
|
|
309
455
|
|
|
456
|
+
Use --consent devops|crm (Datex staff) to grant a missing consent for one
|
|
457
|
+
internal resource without re-running the whole login.
|
|
458
|
+
|
|
310
459
|
\b
|
|
311
460
|
Examples:
|
|
312
461
|
dxs auth login # Login to default tenant
|
|
@@ -314,6 +463,7 @@ def login(
|
|
|
314
463
|
dxs auth login --domain csw-wi.com # Bootstrap: no identity required
|
|
315
464
|
dxs auth login --tenant-id <guid> # Same, when you know the GUID
|
|
316
465
|
dxs auth login CAG --client-id <id> --client-secret <secret>
|
|
466
|
+
dxs auth login --consent devops # Grant the missing DevOps consent only
|
|
317
467
|
"""
|
|
318
468
|
_reject_tenant_override(ctx, "login")
|
|
319
469
|
|
|
@@ -331,6 +481,23 @@ def login(
|
|
|
331
481
|
)
|
|
332
482
|
raise SystemExit(1)
|
|
333
483
|
|
|
484
|
+
if consent_resource:
|
|
485
|
+
if org or tenant_id or domain or client_id or client_secret:
|
|
486
|
+
ctx.output_error(
|
|
487
|
+
ValidationError(
|
|
488
|
+
message=(
|
|
489
|
+
"--consent runs a single consent round for the cached Datex "
|
|
490
|
+
"identity; it cannot be combined with an organization, tenant, "
|
|
491
|
+
"or daemon credentials"
|
|
492
|
+
),
|
|
493
|
+
code="DXS-AUTH-024",
|
|
494
|
+
suggestions=["Run 'dxs auth login --consent devops|crm' by itself"],
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
raise SystemExit(1)
|
|
498
|
+
_run_targeted_consent(ctx, consent_resource)
|
|
499
|
+
return
|
|
500
|
+
|
|
334
501
|
if org and (tenant_id or domain):
|
|
335
502
|
ctx.output_error(
|
|
336
503
|
ValidationError(
|
|
@@ -380,6 +547,7 @@ def login(
|
|
|
380
547
|
include_metadata=False,
|
|
381
548
|
)
|
|
382
549
|
|
|
550
|
+
external_outcomes: list[ConsentOutcome] = []
|
|
383
551
|
try:
|
|
384
552
|
saved_identity = run_standard_device_login(
|
|
385
553
|
ctx,
|
|
@@ -388,12 +556,15 @@ def login(
|
|
|
388
556
|
client_secret=client_secret,
|
|
389
557
|
tenant_id=tenant_id,
|
|
390
558
|
domain=domain,
|
|
559
|
+
consent_outcomes=external_outcomes,
|
|
391
560
|
)
|
|
392
561
|
except AuthenticationError as e:
|
|
393
562
|
ctx.output_error(e)
|
|
394
563
|
raise SystemExit(1) from None
|
|
395
564
|
|
|
396
|
-
external_success_item = _auth_success_item(
|
|
565
|
+
external_success_item = _auth_success_item(
|
|
566
|
+
saved_identity, consent_outcomes=external_outcomes
|
|
567
|
+
)
|
|
397
568
|
ctx.output(single(item=external_success_item, semantic_key="authentication"))
|
|
398
569
|
return
|
|
399
570
|
|
|
@@ -438,6 +609,7 @@ def login(
|
|
|
438
609
|
include_metadata=False,
|
|
439
610
|
)
|
|
440
611
|
|
|
612
|
+
login_outcomes: list[ConsentOutcome] = []
|
|
441
613
|
try:
|
|
442
614
|
saved_identity = run_standard_device_login(
|
|
443
615
|
ctx,
|
|
@@ -445,12 +617,13 @@ def login(
|
|
|
445
617
|
client_id=client_id,
|
|
446
618
|
client_secret=client_secret,
|
|
447
619
|
on_additional_consent_needed=on_additional_consent_needed,
|
|
620
|
+
consent_outcomes=login_outcomes,
|
|
448
621
|
)
|
|
449
622
|
except AuthenticationError as e:
|
|
450
623
|
ctx.output_error(e)
|
|
451
624
|
raise SystemExit(1) from None
|
|
452
625
|
|
|
453
|
-
success_item = _auth_success_item(saved_identity)
|
|
626
|
+
success_item = _auth_success_item(saved_identity, consent_outcomes=login_outcomes)
|
|
454
627
|
ctx.output(single(item=success_item, semantic_key="authentication"))
|
|
455
628
|
|
|
456
629
|
|
|
@@ -1415,7 +1588,9 @@ def _switch_by_organization(
|
|
|
1415
1588
|
# Home-tenant org (Datex, or an org with no tenant of its own): the
|
|
1416
1589
|
# current identity's tokens already cover it, but the org binding on
|
|
1417
1590
|
# the cached identity must still be re-pointed at the requested org.
|
|
1418
|
-
_rebind_home_tenant_identity(
|
|
1591
|
+
_rebind_home_tenant_identity(
|
|
1592
|
+
ctx, cache, org_info, client_id=client_id, client_secret=client_secret
|
|
1593
|
+
)
|
|
1419
1594
|
return
|
|
1420
1595
|
|
|
1421
1596
|
# Step 4: Reject a misregistered home-tenant org BEFORE any identity lookup —
|
|
@@ -1551,6 +1726,8 @@ def _rebind_home_tenant_identity(
|
|
|
1551
1726
|
ctx: DxsContext,
|
|
1552
1727
|
cache: MultiIdentityTokenCache,
|
|
1553
1728
|
org_info: Any,
|
|
1729
|
+
client_id: str | None = None,
|
|
1730
|
+
client_secret: str | None = None,
|
|
1554
1731
|
) -> None:
|
|
1555
1732
|
"""Re-point the active home-tenant identity at ``org_info`` (issue #112).
|
|
1556
1733
|
|
|
@@ -1561,6 +1738,10 @@ def _rebind_home_tenant_identity(
|
|
|
1561
1738
|
made the silent no-op hard to spot — while writes fail with a misleading
|
|
1562
1739
|
``DXS-API-400``. Only the two org fields move; tokens and the active
|
|
1563
1740
|
identity selection stay untouched (the issue #83 contract).
|
|
1741
|
+
|
|
1742
|
+
Supplied daemon credentials are persisted on the active identity on both
|
|
1743
|
+
outcomes (issue #133) — the same contract the other org-switch branches
|
|
1744
|
+
honor. Without credentials, the already-bound outcome writes nothing.
|
|
1564
1745
|
"""
|
|
1565
1746
|
active = cache.get_active_identity()
|
|
1566
1747
|
if active is None or identity_needs_tenant_authority(active):
|
|
@@ -1587,27 +1768,29 @@ def _rebind_home_tenant_identity(
|
|
|
1587
1768
|
)
|
|
1588
1769
|
raise SystemExit(1)
|
|
1589
1770
|
|
|
1771
|
+
storage_key = cache.resolve_stored_key(active) or active.identity_key
|
|
1772
|
+
|
|
1590
1773
|
already_bound = active.organization_id == org_info.id and (
|
|
1591
1774
|
(active.organization_name or "").lower() == org_info.name.lower()
|
|
1592
1775
|
)
|
|
1593
1776
|
if already_bound:
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1777
|
+
identity = active
|
|
1778
|
+
if client_id and client_secret:
|
|
1779
|
+
with_creds = cache.update_identity_daemon_creds(storage_key, client_id, client_secret)
|
|
1780
|
+
if with_creds is not None:
|
|
1781
|
+
identity = with_creds
|
|
1782
|
+
item: dict[str, Any] = {
|
|
1783
|
+
"status": "no_action_required",
|
|
1784
|
+
"message": (f"Active identity is already bound to organization '{org_info.name}'"),
|
|
1785
|
+
"active_identity": active.account_username,
|
|
1786
|
+
"organization": org_info.name,
|
|
1787
|
+
"organization_id": org_info.id,
|
|
1788
|
+
}
|
|
1789
|
+
if identity.footprint_client_id:
|
|
1790
|
+
item["footprint_daemon_auth"] = "configured"
|
|
1791
|
+
ctx.output(single(item=item, semantic_key="authentication"))
|
|
1608
1792
|
return
|
|
1609
1793
|
|
|
1610
|
-
storage_key = cache.resolve_stored_key(active) or active.identity_key
|
|
1611
1794
|
updated = cache.update_identity_org_info(storage_key, org_info.id, org_info.name)
|
|
1612
1795
|
if updated is None:
|
|
1613
1796
|
ctx.output_error(
|
|
@@ -1618,23 +1801,26 @@ def _rebind_home_tenant_identity(
|
|
|
1618
1801
|
)
|
|
1619
1802
|
raise SystemExit(1)
|
|
1620
1803
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1804
|
+
if client_id and client_secret:
|
|
1805
|
+
with_creds = cache.update_identity_daemon_creds(storage_key, client_id, client_secret)
|
|
1806
|
+
if with_creds is not None:
|
|
1807
|
+
updated = with_creds
|
|
1808
|
+
|
|
1809
|
+
item = {
|
|
1810
|
+
"status": "organization_rebound",
|
|
1811
|
+
"message": (
|
|
1812
|
+
f"Re-bound the active identity to organization '{org_info.name}' "
|
|
1813
|
+
"(home tenant; no login required)"
|
|
1814
|
+
),
|
|
1815
|
+
"active_identity": active.account_username,
|
|
1816
|
+
"organization": org_info.name,
|
|
1817
|
+
"organization_id": org_info.id,
|
|
1818
|
+
"previous_organization": active.organization_name,
|
|
1819
|
+
"previous_organization_id": active.organization_id,
|
|
1820
|
+
}
|
|
1821
|
+
if updated.footprint_client_id:
|
|
1822
|
+
item["footprint_daemon_auth"] = "configured"
|
|
1823
|
+
ctx.output(single(item=item, semantic_key="authentication"))
|
|
1638
1824
|
|
|
1639
1825
|
|
|
1640
1826
|
def _handled_home_tenant_external_org(ctx: DxsContext, org_info: Any) -> bool:
|
|
@@ -1726,6 +1912,7 @@ def _authenticate_external_tenant(
|
|
|
1726
1912
|
include_metadata=False,
|
|
1727
1913
|
)
|
|
1728
1914
|
|
|
1915
|
+
org_outcomes: list[ConsentOutcome] = []
|
|
1729
1916
|
try:
|
|
1730
1917
|
ctx.log(f"Initiating device code authentication for {org_info.name}...")
|
|
1731
1918
|
saved_identity = run_standard_device_login(
|
|
@@ -1739,11 +1926,13 @@ def _authenticate_external_tenant(
|
|
|
1739
1926
|
external_entra_id=org_info.external_entra_id,
|
|
1740
1927
|
organization_id=org_info.id,
|
|
1741
1928
|
organization_name=org_info.name,
|
|
1929
|
+
consent_outcomes=org_outcomes,
|
|
1742
1930
|
)
|
|
1743
1931
|
|
|
1744
1932
|
success_item = _auth_success_item(
|
|
1745
1933
|
saved_identity,
|
|
1746
1934
|
message=f"Successfully authenticated with {org_info.name}",
|
|
1935
|
+
consent_outcomes=org_outcomes,
|
|
1747
1936
|
)
|
|
1748
1937
|
ctx.output(single(item=success_item, semantic_key="authentication"))
|
|
1749
1938
|
|
|
@@ -337,7 +337,7 @@ def _build_config_index(client: ApiClient, branch_id: int, ctx: DxsContext) -> C
|
|
|
337
337
|
)
|
|
338
338
|
except ApiError as e:
|
|
339
339
|
# Log JSON parse errors as warnings (e.g., endpoints type)
|
|
340
|
-
if e.code
|
|
340
|
+
if e.code in ("DXS-API-JSON-001", "DXS-API-ROUTE-404"):
|
|
341
341
|
ctx.log(f"Warning: {ctype} returned non-JSON response")
|
|
342
342
|
# Other API errors are expected (type may not exist)
|
|
343
343
|
except Exception:
|