datex-studio-cli 0.4.1__tar.gz → 0.4.2__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.1 → datex_studio_cli-0.4.2}/PKG-INFO +1 -1
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/pyproject.toml +1 -1
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/__init__.py +1 -1
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/cli.py +79 -8
- datex_studio_cli-0.4.2/src/dxs/commands/api.py +236 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/auth.py +114 -35
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/config.py +7 -3
- datex_studio_cli-0.4.2/src/dxs/commands/configuration.py +248 -0
- datex_studio_cli-0.4.2/src/dxs/commands/telemetry.py +183 -0
- datex_studio_cli-0.4.2/src/dxs/core/telemetry/__init__.py +29 -0
- datex_studio_cli-0.4.2/src/dxs/core/telemetry/config.py +143 -0
- datex_studio_cli-0.4.2/src/dxs/core/telemetry/identity.py +89 -0
- datex_studio_cli-0.4.2/src/dxs/core/telemetry/recorder.py +230 -0
- datex_studio_cli-0.4.2/src/dxs/core/telemetry/scrubber.py +83 -0
- datex_studio_cli-0.4.2/src/dxs/core/telemetry/sender.py +215 -0
- datex_studio_cli-0.4.1/src/dxs/commands/api.py +0 -147
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/.gitignore +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/README.md +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/report-creator.skill +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/scripts/analyze_corpus.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/scripts/generate_rdlx_schema.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/__main__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/branch.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/crm.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/datasource.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/devops.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/document.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/endpoint.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/env.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/explore.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/function.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/marketplace.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/odata.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/organization.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/proxy.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/release_notes.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/repo.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/add_cmds.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/api.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/batch.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/create.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/data.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/dataset.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/edit.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/folder.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/preview.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/report/schema_cmds.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/schema.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/servicepack.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/source.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/studio.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/commands/user.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/context.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/api/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/api/client.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/api/endpoints.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/api/metadata_models.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/api/models.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/auth/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/auth/decorators.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/auth/msal_client.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/auth/token_cache.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/cache.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/datasource/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/datasource/flow_generator.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/datasource/flow_models.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/datasource/generator.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/datasource/models.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/datasource/parsers.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/datasource/resolver.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/datasource/type_def_parser.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/datasource/validator.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/designer/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/designer/flow_models.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/designer/models.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/designer/parsers.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/devops/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/devops/client.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/devops/helpers.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/devops/models.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/dynamics/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/dynamics/client.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/footprint/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/footprint/client.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/footprint/edmx_parser.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/footprint/metadata.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/function/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/function/generator.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/function/models.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/function/validator.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/graph.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/output/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/output/csv_fmt.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/output/formatter.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/output/json_fmt.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/output/redaction.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/output/yaml_fmt.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/release_notes.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/core/responses.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/models/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/datasource_binding.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/engine.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/field_parser.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/folder.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/manifest.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/models.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/preview.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/preview_arjs.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/schema.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/svg_renderer.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/templates/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/validate_arjs.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/validator.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/report/wrapper.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/click_options.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/config.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/errors.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/filtering.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/image.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/paths.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/resolvers.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/responses.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/restricted.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/sorting.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/utils/update_check.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/__init__.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/app.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/design.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/proxy_app.py +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/404/index.html +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/404.html +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-a5e129c2d223432c.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/app/design/page-21ba66a3dd3b03f0.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/design/capture/index.html +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/design/capture/index.txt +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/design/index.html +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/design/index.txt +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/index.html +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/index.txt +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/render-cli.js +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/report-preview.html +0 -0
- {datex_studio_cli-0.4.1 → datex_studio_cli-0.4.2}/src/dxs/web/static/report-validate.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datex-studio-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -260,6 +260,17 @@ def cli(
|
|
|
260
260
|
# Stash for the result_callback to honor --no-update-check
|
|
261
261
|
dxs_ctx.no_update_check = no_update_check
|
|
262
262
|
|
|
263
|
+
# Telemetry: record invocation start (must come before any subcommand runs).
|
|
264
|
+
# Wrapped so a telemetry bug can never block the CLI.
|
|
265
|
+
try:
|
|
266
|
+
from dxs.core.telemetry import maybe_show_first_run_notice, record_start
|
|
267
|
+
|
|
268
|
+
invoked = ctx.invoked_subcommand or "<root>"
|
|
269
|
+
record_start(argv=sys.argv[1:], command=invoked)
|
|
270
|
+
maybe_show_first_run_notice()
|
|
271
|
+
except Exception: # noqa: BLE001
|
|
272
|
+
pass
|
|
273
|
+
|
|
263
274
|
|
|
264
275
|
def _mask_token(token: str | None, visible_chars: int = 8) -> str:
|
|
265
276
|
"""Mask a token for display, showing only first/last few characters.
|
|
@@ -316,18 +327,30 @@ def handle_errors(f: F) -> F:
|
|
|
316
327
|
try:
|
|
317
328
|
return ctx.invoke(f, *args, **kwargs)
|
|
318
329
|
except DxsError as e:
|
|
330
|
+
_record_error_silently(e)
|
|
319
331
|
dxs_ctx.output_error(e)
|
|
320
332
|
sys.exit(1)
|
|
321
333
|
except click.ClickException:
|
|
322
334
|
# Let Click handle its own exceptions
|
|
323
335
|
raise
|
|
324
336
|
except Exception as e:
|
|
337
|
+
_record_error_silently(e)
|
|
325
338
|
dxs_ctx.output_error(e)
|
|
326
339
|
sys.exit(1)
|
|
327
340
|
|
|
328
341
|
return cast(F, wrapper)
|
|
329
342
|
|
|
330
343
|
|
|
344
|
+
def _record_error_silently(exc: BaseException) -> None:
|
|
345
|
+
"""Emit a telemetry error event; never raises."""
|
|
346
|
+
try:
|
|
347
|
+
from dxs.core.telemetry import record_error
|
|
348
|
+
|
|
349
|
+
record_error(exc)
|
|
350
|
+
except Exception: # noqa: BLE001
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
|
|
331
354
|
# Import and register command groups
|
|
332
355
|
# These imports are here to avoid circular imports
|
|
333
356
|
def register_commands() -> None:
|
|
@@ -336,6 +359,7 @@ def register_commands() -> None:
|
|
|
336
359
|
api,
|
|
337
360
|
auth,
|
|
338
361
|
config,
|
|
362
|
+
configuration,
|
|
339
363
|
crm,
|
|
340
364
|
datasource,
|
|
341
365
|
devops,
|
|
@@ -355,6 +379,8 @@ def register_commands() -> None:
|
|
|
355
379
|
cli.add_command(api.api)
|
|
356
380
|
cli.add_command(auth.auth)
|
|
357
381
|
cli.add_command(config.config)
|
|
382
|
+
cli.add_command(config.config, name="settings")
|
|
383
|
+
cli.add_command(configuration.configuration)
|
|
358
384
|
cli.add_command(crm.crm)
|
|
359
385
|
cli.add_command(datasource.datasource)
|
|
360
386
|
cli.add_command(devops.devops)
|
|
@@ -376,14 +402,30 @@ def register_commands() -> None:
|
|
|
376
402
|
|
|
377
403
|
cli.add_command(schema.schema)
|
|
378
404
|
|
|
405
|
+
# Telemetry subcommands (status / enable / disable / test)
|
|
406
|
+
from dxs.commands import telemetry as telemetry_cmd
|
|
407
|
+
|
|
408
|
+
cli.add_command(telemetry_cmd.telemetry)
|
|
409
|
+
|
|
379
410
|
|
|
380
411
|
@cli.result_callback()
|
|
381
412
|
@click.pass_context
|
|
382
413
|
def _after_command(ctx: click.Context, result: Any, **kwargs: Any) -> Any:
|
|
383
|
-
"""Run post-command hooks (
|
|
414
|
+
"""Run post-command hooks (telemetry success event + PyPI update notice)."""
|
|
384
415
|
from dxs.utils.update_check import maybe_show_update_notice
|
|
385
416
|
|
|
386
417
|
dxs_ctx: DxsContext | None = ctx.find_object(DxsContext)
|
|
418
|
+
|
|
419
|
+
# Emit a success event. handle_errors-decorated commands never reach here
|
|
420
|
+
# on failure, so the only path through this branch is a successful run.
|
|
421
|
+
try:
|
|
422
|
+
from dxs.core.telemetry import record_end
|
|
423
|
+
|
|
424
|
+
ctx_extras = _ctx_extras(dxs_ctx)
|
|
425
|
+
record_end(exit_code=0, ctx_extras=ctx_extras)
|
|
426
|
+
except Exception: # noqa: BLE001
|
|
427
|
+
pass
|
|
428
|
+
|
|
387
429
|
skip = bool(dxs_ctx and dxs_ctx.no_update_check)
|
|
388
430
|
try:
|
|
389
431
|
maybe_show_update_notice(skip=skip)
|
|
@@ -392,6 +434,18 @@ def _after_command(ctx: click.Context, result: Any, **kwargs: Any) -> Any:
|
|
|
392
434
|
return result
|
|
393
435
|
|
|
394
436
|
|
|
437
|
+
def _ctx_extras(dxs_ctx: "DxsContext | None") -> dict[str, Any]:
|
|
438
|
+
"""Pluck branch/repo/org/env from DxsContext for telemetry events."""
|
|
439
|
+
if dxs_ctx is None:
|
|
440
|
+
return {}
|
|
441
|
+
return {
|
|
442
|
+
"branch_id": dxs_ctx.branch,
|
|
443
|
+
"repo_id": dxs_ctx.repo,
|
|
444
|
+
"org": dxs_ctx.org,
|
|
445
|
+
"env": dxs_ctx.env,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
|
|
395
449
|
@cli.command("update")
|
|
396
450
|
@click.option(
|
|
397
451
|
"--check",
|
|
@@ -438,13 +492,30 @@ register_commands()
|
|
|
438
492
|
def main() -> None:
|
|
439
493
|
"""Main entry point for the CLI."""
|
|
440
494
|
try:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
495
|
+
try:
|
|
496
|
+
cli()
|
|
497
|
+
except DxsError as e:
|
|
498
|
+
_record_error_silently(e)
|
|
499
|
+
# Use the module-level format captured during CLI init
|
|
500
|
+
# (Click context is torn down before exceptions reach here)
|
|
501
|
+
formatted = format_error(e, _last_output_format)
|
|
502
|
+
click.echo(formatted, err=True)
|
|
503
|
+
sys.exit(1)
|
|
504
|
+
except SystemExit:
|
|
505
|
+
# SystemExit from sys.exit() inside handle_errors — telemetry already
|
|
506
|
+
# recorded an error there; just propagate.
|
|
507
|
+
raise
|
|
508
|
+
except BaseException as e:
|
|
509
|
+
# Anything else escaping cli() shouldn't take telemetry with it.
|
|
510
|
+
_record_error_silently(e)
|
|
511
|
+
raise
|
|
512
|
+
finally:
|
|
513
|
+
try:
|
|
514
|
+
from dxs.core.telemetry import flush
|
|
515
|
+
|
|
516
|
+
flush(timeout=0.5)
|
|
517
|
+
except Exception: # noqa: BLE001
|
|
518
|
+
pass
|
|
448
519
|
|
|
449
520
|
|
|
450
521
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Raw API request command: dxs api [METHOD] [URL] [PAYLOAD]."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from dxs.cli import DxsContext, pass_context
|
|
12
|
+
from dxs.core.auth import get_access_token, require_auth
|
|
13
|
+
from dxs.utils.config import get_settings, is_restricted_mode
|
|
14
|
+
from dxs.utils.errors import ApiError, RestrictedModeError
|
|
15
|
+
from dxs.utils.responses import single
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command()
|
|
19
|
+
@click.argument(
|
|
20
|
+
"method", type=click.Choice(["GET", "POST", "PUT", "PATCH", "DELETE"], case_sensitive=False)
|
|
21
|
+
)
|
|
22
|
+
@click.argument("url")
|
|
23
|
+
@click.argument("payload", required=False, default=None)
|
|
24
|
+
@click.option(
|
|
25
|
+
"--data-file",
|
|
26
|
+
"-D",
|
|
27
|
+
"data_file",
|
|
28
|
+
type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
|
|
29
|
+
default=None,
|
|
30
|
+
help="Read JSON request body from a file (avoids shell escaping for large payloads).",
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
"--output-file",
|
|
34
|
+
"-O",
|
|
35
|
+
"output_file",
|
|
36
|
+
type=click.Path(dir_okay=False, writable=True, path_type=Path),
|
|
37
|
+
default=None,
|
|
38
|
+
help="Write the response body to a file. Implies --raw.",
|
|
39
|
+
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--raw",
|
|
42
|
+
is_flag=True,
|
|
43
|
+
default=False,
|
|
44
|
+
help="Print only the response body (no envelope) as JSON to stdout. Useful for piping to jq.",
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--headers",
|
|
48
|
+
"-H",
|
|
49
|
+
multiple=True,
|
|
50
|
+
help="Additional headers in format 'Key: Value'",
|
|
51
|
+
)
|
|
52
|
+
@pass_context
|
|
53
|
+
@require_auth
|
|
54
|
+
def api(
|
|
55
|
+
ctx: DxsContext,
|
|
56
|
+
method: str,
|
|
57
|
+
url: str,
|
|
58
|
+
payload: str | None,
|
|
59
|
+
data_file: Path | None,
|
|
60
|
+
output_file: Path | None,
|
|
61
|
+
raw: bool,
|
|
62
|
+
headers: tuple[str, ...],
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Make raw API requests with automatic authentication.
|
|
65
|
+
|
|
66
|
+
Automatically adds Bearer token authentication and handles the HTTP request.
|
|
67
|
+
Useful for testing and exploring API endpoints.
|
|
68
|
+
|
|
69
|
+
\b
|
|
70
|
+
Arguments:
|
|
71
|
+
METHOD HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
72
|
+
URL Full URL or relative path (e.g., /organizations/mine)
|
|
73
|
+
PAYLOAD JSON payload for POST/PUT/PATCH (optional; alternative: --data-file)
|
|
74
|
+
|
|
75
|
+
\b
|
|
76
|
+
Options:
|
|
77
|
+
-D, --data-file Read JSON body from a file (recommended for large payloads)
|
|
78
|
+
-O, --output-file Write response body to a file (implies --raw)
|
|
79
|
+
--raw Emit only the response body (no envelope) for piping
|
|
80
|
+
-H, --headers Additional headers (repeatable)
|
|
81
|
+
|
|
82
|
+
\b
|
|
83
|
+
Examples:
|
|
84
|
+
dxs api GET /organizations/mine
|
|
85
|
+
dxs api GET /applications/64/hubconfigurations/8642750 --raw -O hub.json
|
|
86
|
+
dxs api PUT /applications/64/hubconfigurations/8642750 --data-file hub.json
|
|
87
|
+
dxs api POST /applications -D payload.json
|
|
88
|
+
dxs api GET /branches -H "X-Custom: value"
|
|
89
|
+
"""
|
|
90
|
+
if payload is not None and data_file is not None:
|
|
91
|
+
raise ApiError(
|
|
92
|
+
message="Pass either a positional PAYLOAD or --data-file, not both.",
|
|
93
|
+
code="DXS-API-PAYLOAD-002",
|
|
94
|
+
suggestions=["Drop one of the two arguments and re-run."],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# --output-file implies --raw (writing an envelope to disk would defeat the purpose).
|
|
98
|
+
if output_file is not None:
|
|
99
|
+
raw = True
|
|
100
|
+
|
|
101
|
+
# Block mutating methods in restricted mode
|
|
102
|
+
if is_restricted_mode() and method.upper() != "GET":
|
|
103
|
+
raise RestrictedModeError(
|
|
104
|
+
command_name=f"api {method.upper()}",
|
|
105
|
+
reason="performs mutating API requests",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
settings = get_settings()
|
|
109
|
+
token = get_access_token()
|
|
110
|
+
|
|
111
|
+
# Determine if URL is relative or absolute
|
|
112
|
+
if url.startswith("http://") or url.startswith("https://"):
|
|
113
|
+
full_url = url
|
|
114
|
+
else:
|
|
115
|
+
# Relative path - prepend base URL
|
|
116
|
+
base_url = settings.api_base_url.rstrip("/")
|
|
117
|
+
path = url if url.startswith("/") else f"/{url}"
|
|
118
|
+
full_url = f"{base_url}{path}"
|
|
119
|
+
|
|
120
|
+
ctx.log(f"Making {method} request to {full_url}")
|
|
121
|
+
|
|
122
|
+
# Build headers
|
|
123
|
+
request_headers = {
|
|
124
|
+
"Authorization": f"Bearer {token}",
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
"Accept": "application/json",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Parse additional headers
|
|
130
|
+
for header in headers:
|
|
131
|
+
if ":" in header:
|
|
132
|
+
key, value = header.split(":", 1)
|
|
133
|
+
request_headers[key.strip()] = value.strip()
|
|
134
|
+
|
|
135
|
+
# Resolve and parse payload from positional arg or --data-file
|
|
136
|
+
payload_data = None
|
|
137
|
+
payload_source: str | None = None
|
|
138
|
+
if data_file is not None:
|
|
139
|
+
payload_source = f"--data-file {data_file}"
|
|
140
|
+
try:
|
|
141
|
+
payload_data = json.loads(data_file.read_text(encoding="utf-8"))
|
|
142
|
+
except json.JSONDecodeError as e:
|
|
143
|
+
raise ApiError(
|
|
144
|
+
message=f"Invalid JSON in {data_file}: {e}",
|
|
145
|
+
code="DXS-API-PAYLOAD-001",
|
|
146
|
+
suggestions=[f"Validate the file with: python -m json.tool {data_file}"],
|
|
147
|
+
) from e
|
|
148
|
+
elif payload:
|
|
149
|
+
payload_source = "positional argument"
|
|
150
|
+
try:
|
|
151
|
+
payload_data = json.loads(payload)
|
|
152
|
+
except json.JSONDecodeError as e:
|
|
153
|
+
raise ApiError(
|
|
154
|
+
message=f"Invalid JSON payload: {e}",
|
|
155
|
+
code="DXS-API-PAYLOAD-001",
|
|
156
|
+
suggestions=[
|
|
157
|
+
"Ensure the payload is valid JSON",
|
|
158
|
+
"For large or complex bodies, use --data-file PATH instead.",
|
|
159
|
+
],
|
|
160
|
+
) from e
|
|
161
|
+
|
|
162
|
+
# Make the request
|
|
163
|
+
try:
|
|
164
|
+
with httpx.Client(timeout=settings.api_timeout, verify=settings.verify_ssl) as client:
|
|
165
|
+
response = client.request(
|
|
166
|
+
method=method.upper(),
|
|
167
|
+
url=full_url,
|
|
168
|
+
headers=request_headers,
|
|
169
|
+
json=payload_data if payload_data else None,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Decode body once
|
|
173
|
+
try:
|
|
174
|
+
body: Any = response.json()
|
|
175
|
+
body_is_json = True
|
|
176
|
+
except Exception:
|
|
177
|
+
body = response.text
|
|
178
|
+
body_is_json = False
|
|
179
|
+
|
|
180
|
+
if raw:
|
|
181
|
+
_emit_raw(ctx, body, body_is_json, output_file)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
response_data: dict[str, Any] = {
|
|
185
|
+
"status_code": response.status_code,
|
|
186
|
+
"headers": dict(response.headers),
|
|
187
|
+
"url": str(response.url),
|
|
188
|
+
"method": method.upper(),
|
|
189
|
+
"body": body,
|
|
190
|
+
"request": {
|
|
191
|
+
"url": full_url,
|
|
192
|
+
"method": method.upper(),
|
|
193
|
+
"headers": request_headers,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
if payload_data:
|
|
197
|
+
response_data["request"]["payload"] = payload_data
|
|
198
|
+
if payload_source:
|
|
199
|
+
response_data["request"]["payload_source"] = payload_source
|
|
200
|
+
|
|
201
|
+
ctx.output(
|
|
202
|
+
single(
|
|
203
|
+
item=response_data,
|
|
204
|
+
semantic_key="api_response",
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
except httpx.RequestError as e:
|
|
209
|
+
raise ApiError(
|
|
210
|
+
message=f"Request failed: {e}",
|
|
211
|
+
code="DXS-API-REQUEST-001",
|
|
212
|
+
details={"url": full_url, "error": str(e)},
|
|
213
|
+
) from e
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _emit_raw(
|
|
217
|
+
ctx: DxsContext,
|
|
218
|
+
body: Any,
|
|
219
|
+
body_is_json: bool,
|
|
220
|
+
output_file: Path | None,
|
|
221
|
+
) -> None:
|
|
222
|
+
"""Print or save the response body without the envelope."""
|
|
223
|
+
if body_is_json:
|
|
224
|
+
rendered = json.dumps(body, indent=2, sort_keys=False, ensure_ascii=False)
|
|
225
|
+
else:
|
|
226
|
+
rendered = body if isinstance(body, str) else str(body)
|
|
227
|
+
|
|
228
|
+
if output_file is not None:
|
|
229
|
+
output_file.write_text(rendered, encoding="utf-8")
|
|
230
|
+
ctx.log(f"Wrote response body to {output_file}")
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
sys.stdout.write(rendered)
|
|
234
|
+
if not rendered.endswith("\n"):
|
|
235
|
+
sys.stdout.write("\n")
|
|
236
|
+
sys.stdout.flush()
|
|
@@ -581,8 +581,23 @@ def list_identities(ctx: DxsContext) -> None:
|
|
|
581
581
|
|
|
582
582
|
@auth.command()
|
|
583
583
|
@click.argument("identifier")
|
|
584
|
+
@click.option(
|
|
585
|
+
"--client-id",
|
|
586
|
+
default=None,
|
|
587
|
+
help="Footprint API app registration client ID (for daemon auth)",
|
|
588
|
+
)
|
|
589
|
+
@click.option(
|
|
590
|
+
"--client-secret",
|
|
591
|
+
default=None,
|
|
592
|
+
help="Footprint API app registration client secret (for daemon auth)",
|
|
593
|
+
)
|
|
584
594
|
@pass_context
|
|
585
|
-
def switch(
|
|
595
|
+
def switch(
|
|
596
|
+
ctx: DxsContext,
|
|
597
|
+
identifier: str,
|
|
598
|
+
client_id: str | None,
|
|
599
|
+
client_secret: str | None,
|
|
600
|
+
) -> None:
|
|
586
601
|
"""Switch to a different identity or organization.
|
|
587
602
|
|
|
588
603
|
Auto-detects the identifier type:
|
|
@@ -593,15 +608,35 @@ def switch(ctx: DxsContext, identifier: str) -> None:
|
|
|
593
608
|
this command will initiate device code flow to authenticate against the
|
|
594
609
|
external tenant if no cached identity exists for that tenant.
|
|
595
610
|
|
|
611
|
+
Use --client-id and --client-secret together to configure daemon (client
|
|
612
|
+
credentials) authentication for the Footprint API on the target identity.
|
|
613
|
+
Required for cross-tenant access where OBO flow is not available. The
|
|
614
|
+
credentials are stored on the identity and reused for subsequent OData
|
|
615
|
+
queries.
|
|
616
|
+
|
|
596
617
|
\b
|
|
597
618
|
Examples:
|
|
598
619
|
dxs auth switch user@acme.com # Switch by username (email)
|
|
599
620
|
dxs auth switch CAG # Switch by organization name
|
|
621
|
+
dxs auth switch CAG --client-id <id> --client-secret <secret>
|
|
600
622
|
"""
|
|
623
|
+
if bool(client_id) != bool(client_secret):
|
|
624
|
+
ctx.output_error(
|
|
625
|
+
ValidationError(
|
|
626
|
+
message="--client-id and --client-secret must be provided together",
|
|
627
|
+
code="DXS-AUTH-015",
|
|
628
|
+
suggestions=[
|
|
629
|
+
"Provide both --client-id and --client-secret for daemon auth",
|
|
630
|
+
"Or omit both to switch without changing daemon credentials",
|
|
631
|
+
],
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
raise SystemExit(1)
|
|
635
|
+
|
|
601
636
|
if "@" in identifier:
|
|
602
|
-
_switch_by_username(ctx, identifier)
|
|
637
|
+
_switch_by_username(ctx, identifier, client_id=client_id, client_secret=client_secret)
|
|
603
638
|
else:
|
|
604
|
-
_switch_by_organization(ctx, identifier)
|
|
639
|
+
_switch_by_organization(ctx, identifier, client_id=client_id, client_secret=client_secret)
|
|
605
640
|
|
|
606
641
|
|
|
607
642
|
@auth.command()
|
|
@@ -681,7 +716,11 @@ def consent(ctx: DxsContext, org: str, open: bool) -> None:
|
|
|
681
716
|
footprint_consent_url = f"{base_consent}?client_id={fp_client_id}" if fp_client_id else None
|
|
682
717
|
|
|
683
718
|
consent_apps = [
|
|
684
|
-
{
|
|
719
|
+
{
|
|
720
|
+
"name": "Datex Studio API",
|
|
721
|
+
"client_id": settings.azure_client_id,
|
|
722
|
+
"url": datex_consent_url,
|
|
723
|
+
},
|
|
685
724
|
]
|
|
686
725
|
if footprint_consent_url and fp_client_id:
|
|
687
726
|
consent_apps.append(
|
|
@@ -867,7 +906,12 @@ def _build_token_status(claims: dict[str, Any]) -> dict[str, Any]:
|
|
|
867
906
|
return status
|
|
868
907
|
|
|
869
908
|
|
|
870
|
-
def _switch_by_username(
|
|
909
|
+
def _switch_by_username(
|
|
910
|
+
ctx: DxsContext,
|
|
911
|
+
username: str,
|
|
912
|
+
client_id: str | None = None,
|
|
913
|
+
client_secret: str | None = None,
|
|
914
|
+
) -> None:
|
|
871
915
|
"""Switch to an identity by username (original behavior)."""
|
|
872
916
|
cache = MultiIdentityTokenCache()
|
|
873
917
|
|
|
@@ -922,22 +966,32 @@ def _switch_by_username(ctx: DxsContext, username: str) -> None:
|
|
|
922
966
|
)
|
|
923
967
|
raise SystemExit(1) from None
|
|
924
968
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
969
|
+
# Persist daemon credentials on the identity if supplied
|
|
970
|
+
if client_id and client_secret:
|
|
971
|
+
updated = cache.update_identity_daemon_creds(username, client_id, client_secret)
|
|
972
|
+
if updated is not None:
|
|
973
|
+
new_identity = updated
|
|
974
|
+
|
|
975
|
+
item: dict[str, Any] = {
|
|
976
|
+
"status": "switched",
|
|
977
|
+
"message": f"Switched to {username}",
|
|
978
|
+
"previous_identity": previous_username,
|
|
979
|
+
"active_identity": username,
|
|
980
|
+
"organization": new_identity.organization_name,
|
|
981
|
+
"organization_id": new_identity.organization_id,
|
|
982
|
+
}
|
|
983
|
+
if new_identity.footprint_client_id:
|
|
984
|
+
item["footprint_daemon_auth"] = "configured"
|
|
985
|
+
|
|
986
|
+
ctx.output(single(item=item, semantic_key="authentication"))
|
|
938
987
|
|
|
939
988
|
|
|
940
|
-
def _switch_by_organization(
|
|
989
|
+
def _switch_by_organization(
|
|
990
|
+
ctx: DxsContext,
|
|
991
|
+
org_name: str,
|
|
992
|
+
client_id: str | None = None,
|
|
993
|
+
client_secret: str | None = None,
|
|
994
|
+
) -> None:
|
|
941
995
|
"""Switch to an organization, authenticating against external tenant if needed."""
|
|
942
996
|
|
|
943
997
|
cache = MultiIdentityTokenCache()
|
|
@@ -946,7 +1000,14 @@ def _switch_by_organization(ctx: DxsContext, org_name: str) -> None:
|
|
|
946
1000
|
existing_by_org = cache.get_identity_by_org_name(org_name)
|
|
947
1001
|
if existing_by_org:
|
|
948
1002
|
# We have an identity for this org - switch to it
|
|
949
|
-
_activate_existing_identity(
|
|
1003
|
+
_activate_existing_identity(
|
|
1004
|
+
ctx,
|
|
1005
|
+
cache,
|
|
1006
|
+
existing_by_org,
|
|
1007
|
+
org_name,
|
|
1008
|
+
client_id=client_id,
|
|
1009
|
+
client_secret=client_secret,
|
|
1010
|
+
)
|
|
950
1011
|
return
|
|
951
1012
|
|
|
952
1013
|
# Step 2: Fetch organization details from API
|
|
@@ -992,7 +1053,14 @@ def _switch_by_organization(ctx: DxsContext, org_name: str) -> None:
|
|
|
992
1053
|
org_info.id,
|
|
993
1054
|
org_info.name,
|
|
994
1055
|
)
|
|
995
|
-
_activate_existing_identity(
|
|
1056
|
+
_activate_existing_identity(
|
|
1057
|
+
ctx,
|
|
1058
|
+
cache,
|
|
1059
|
+
existing_by_tenant,
|
|
1060
|
+
org_name,
|
|
1061
|
+
client_id=client_id,
|
|
1062
|
+
client_secret=client_secret,
|
|
1063
|
+
)
|
|
996
1064
|
return
|
|
997
1065
|
|
|
998
1066
|
# Step 5: Need to authenticate against external tenant
|
|
@@ -1006,7 +1074,9 @@ def _switch_by_organization(ctx: DxsContext, org_name: str) -> None:
|
|
|
1006
1074
|
)
|
|
1007
1075
|
raise SystemExit(1)
|
|
1008
1076
|
|
|
1009
|
-
_authenticate_external_tenant(
|
|
1077
|
+
_authenticate_external_tenant(
|
|
1078
|
+
ctx, cache, org_info, client_id=client_id, client_secret=client_secret
|
|
1079
|
+
)
|
|
1010
1080
|
|
|
1011
1081
|
|
|
1012
1082
|
def _activate_existing_identity(
|
|
@@ -1014,6 +1084,8 @@ def _activate_existing_identity(
|
|
|
1014
1084
|
cache: MultiIdentityTokenCache,
|
|
1015
1085
|
identity: Any,
|
|
1016
1086
|
org_name: str,
|
|
1087
|
+
client_id: str | None = None,
|
|
1088
|
+
client_secret: str | None = None,
|
|
1017
1089
|
) -> None:
|
|
1018
1090
|
"""Activate an existing cached identity."""
|
|
1019
1091
|
# Check if token is expired
|
|
@@ -1034,20 +1106,27 @@ def _activate_existing_identity(
|
|
|
1034
1106
|
|
|
1035
1107
|
cache.set_active_identity(identity.identity_key)
|
|
1036
1108
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
"message": f"Switched to organization {org_name}",
|
|
1042
|
-
"previous_identity": previous_username,
|
|
1043
|
-
"active_identity": identity.account_username,
|
|
1044
|
-
"organization": identity.organization_name,
|
|
1045
|
-
"organization_id": identity.organization_id,
|
|
1046
|
-
"external_tenant": identity.external_entra_id,
|
|
1047
|
-
},
|
|
1048
|
-
semantic_key="authentication",
|
|
1109
|
+
# Persist daemon credentials on the identity if supplied
|
|
1110
|
+
if client_id and client_secret and identity.account_username:
|
|
1111
|
+
updated = cache.update_identity_daemon_creds(
|
|
1112
|
+
identity.account_username, client_id, client_secret
|
|
1049
1113
|
)
|
|
1050
|
-
|
|
1114
|
+
if updated is not None:
|
|
1115
|
+
identity = updated
|
|
1116
|
+
|
|
1117
|
+
item: dict[str, Any] = {
|
|
1118
|
+
"status": "switched",
|
|
1119
|
+
"message": f"Switched to organization {org_name}",
|
|
1120
|
+
"previous_identity": previous_username,
|
|
1121
|
+
"active_identity": identity.account_username,
|
|
1122
|
+
"organization": identity.organization_name,
|
|
1123
|
+
"organization_id": identity.organization_id,
|
|
1124
|
+
"external_tenant": identity.external_entra_id,
|
|
1125
|
+
}
|
|
1126
|
+
if identity.footprint_client_id:
|
|
1127
|
+
item["footprint_daemon_auth"] = "configured"
|
|
1128
|
+
|
|
1129
|
+
ctx.output(single(item=item, semantic_key="authentication"))
|
|
1051
1130
|
|
|
1052
1131
|
|
|
1053
1132
|
def _fetch_organization_by_name(ctx: DxsContext, org_name: str) -> Any:
|
|
@@ -10,10 +10,14 @@ from dxs.utils.restricted import restrict_in_restricted_mode
|
|
|
10
10
|
|
|
11
11
|
@click.group()
|
|
12
12
|
def config() -> None:
|
|
13
|
-
"""
|
|
13
|
+
"""CLI settings stored in ~/.datex/config.yaml.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Manages local CLI configuration (api_base_url, default_org, etc.). Settings
|
|
16
|
+
can also be supplied via environment variables (prefixed with DXS_).
|
|
17
|
+
|
|
18
|
+
Aliased as `dxs settings` to disambiguate from `dxs configuration`, which
|
|
19
|
+
operates on platform configuration objects (hubs, grids, forms, flows, etc.)
|
|
20
|
+
on a Datex Studio branch.
|
|
17
21
|
"""
|
|
18
22
|
pass
|
|
19
23
|
|