datex-studio-cli 0.4.9__tar.gz → 0.4.10__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.9 → datex_studio_cli-0.4.10}/PKG-INFO +1 -1
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/pyproject.toml +1 -1
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/__init__.py +1 -1
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/auth.py +51 -4
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/studio.py +85 -19
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/auth/decorators.py +33 -11
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/auth/msal_client.py +34 -6
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/auth/token_cache.py +204 -59
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/client.py +5 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/paths.py +33 -11
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/app.py +3 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/.gitignore +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/README.md +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/report-creator.skill +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/scripts/analyze_corpus.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/scripts/generate_rdlx_schema.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/__main__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/cli.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/api.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/branch.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/config.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/configuration.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/crm.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/datasource.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/devops.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/document.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/endpoint.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/env.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/explore.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/function.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/marketplace.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/odata.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/organization.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/proxy.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/release_notes.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/repo.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/add_cmds.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/api.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/batch.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/create.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/data.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/dataset.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/edit.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/folder.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/preview.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/report/schema_cmds.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/schema.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/servicepack.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/source.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/telemetry.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/commands/user.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/context.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/api/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/api/app_config.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/api/client.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/api/endpoints.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/api/metadata_models.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/api/models.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/auth/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/cache.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/flow_generator.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/flow_models.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/generator.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/models.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/parsers.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/resolver.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/type_def_parser.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/validator.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/designer/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/designer/flow_models.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/designer/models.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/designer/parsers.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/devops/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/devops/client.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/devops/helpers.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/devops/models.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/dynamics/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/dynamics/client.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/dynamics/metadata_cache.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/edmx_parser.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/metadata.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/function/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/function/generator.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/function/models.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/function/validator.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/graph.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/output/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/output/csv_fmt.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/output/formatter.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/output/json_fmt.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/output/redaction.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/output/yaml_fmt.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/release_notes.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/responses.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/config.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/identity.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/recorder.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/scrubber.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/sender.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/models/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/datasource_binding.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/engine.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/field_parser.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/folder.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/manifest.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/models.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/preview.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/preview_arjs.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/schema.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/svg_renderer.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/templates/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/validate_arjs.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/validator.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/report/wrapper.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/click_options.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/config.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/errors.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/filtering.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/image.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/resolvers.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/responses.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/restricted.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/sorting.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/utils/update_check.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/__init__.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/design.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/proxy_app.py +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/404/index.html +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/404.html +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-a5e129c2d223432c.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/design/page-21ba66a3dd3b03f0.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/capture/index.html +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/capture/index.txt +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/index.html +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/index.txt +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/index.html +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/index.txt +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/render-cli.js +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/src/dxs/web/static/report-preview.html +0 -0
- {datex_studio_cli-0.4.9 → datex_studio_cli-0.4.10}/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.10
|
|
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
|
|
@@ -14,6 +14,44 @@ from dxs.utils.responses import list_response, single
|
|
|
14
14
|
from dxs.utils.restricted import restrict_in_restricted_mode
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _format_age(seconds: int) -> str:
|
|
18
|
+
"""Human-readable age from a number of seconds."""
|
|
19
|
+
if seconds < 60:
|
|
20
|
+
return f"{seconds} seconds"
|
|
21
|
+
if seconds < 3600:
|
|
22
|
+
return f"{seconds // 60} minutes"
|
|
23
|
+
if seconds < 86400:
|
|
24
|
+
return f"{seconds // 3600} hours"
|
|
25
|
+
return f"{seconds // 86400} days"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _refresh_token_info(identity: Any, now: datetime) -> dict[str, Any]:
|
|
29
|
+
"""Describe the identity's refresh token: presence and age.
|
|
30
|
+
|
|
31
|
+
Azure does not return a refresh-token expiry, so we surface the age of the
|
|
32
|
+
stored token (recorded when it was obtained) to help diagnose refresh issues.
|
|
33
|
+
"""
|
|
34
|
+
if not identity.refresh_token:
|
|
35
|
+
return {
|
|
36
|
+
"present": False,
|
|
37
|
+
"note": "No refresh token stored; a full login is required.",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
info: dict[str, Any] = {
|
|
41
|
+
"present": True,
|
|
42
|
+
"note": "Azure does not expose refresh-token expiry; age shown for reference.",
|
|
43
|
+
}
|
|
44
|
+
acquired_at = identity.refresh_token_acquired_at
|
|
45
|
+
if acquired_at is not None:
|
|
46
|
+
age_seconds = int((now - acquired_at).total_seconds())
|
|
47
|
+
info["acquired_at"] = acquired_at.isoformat()
|
|
48
|
+
info["age_human"] = _format_age(max(age_seconds, 0))
|
|
49
|
+
else:
|
|
50
|
+
info["acquired_at"] = None
|
|
51
|
+
info["age_human"] = "unknown (log in again to start tracking age)"
|
|
52
|
+
return info
|
|
53
|
+
|
|
54
|
+
|
|
17
55
|
@click.group()
|
|
18
56
|
def auth() -> None:
|
|
19
57
|
"""Authentication commands.
|
|
@@ -398,8 +436,8 @@ def status(ctx: DxsContext) -> None:
|
|
|
398
436
|
# If primary token is expired, try to refresh it
|
|
399
437
|
if is_expired:
|
|
400
438
|
ctx.log("Primary access token is expired, attempting to refresh...")
|
|
401
|
-
|
|
402
|
-
if
|
|
439
|
+
outcome = cache.try_refresh_detailed()
|
|
440
|
+
if outcome.access_token:
|
|
403
441
|
ctx.log("Successfully refreshed access token")
|
|
404
442
|
refreshed_identity = cache.get_active_identity()
|
|
405
443
|
if refreshed_identity:
|
|
@@ -415,6 +453,9 @@ def status(ctx: DxsContext) -> None:
|
|
|
415
453
|
"message": "Authentication token has expired and refresh failed",
|
|
416
454
|
"account": active_identity.account_username,
|
|
417
455
|
"expired_at": active_identity.expires_at.isoformat(),
|
|
456
|
+
"refresh_error": outcome.error,
|
|
457
|
+
"refresh_error_description": outcome.error_description,
|
|
458
|
+
"refresh_token": _refresh_token_info(active_identity, now),
|
|
418
459
|
"suggestion": "Run 'dxs auth login' to re-authenticate",
|
|
419
460
|
},
|
|
420
461
|
semantic_key="authentication",
|
|
@@ -430,9 +471,14 @@ def status(ctx: DxsContext) -> None:
|
|
|
430
471
|
else:
|
|
431
472
|
return f"{seconds // 3600} hours"
|
|
432
473
|
|
|
433
|
-
# Check all cached tokens for each resource
|
|
474
|
+
# Check all cached tokens for each resource.
|
|
475
|
+
# External-tenant identities must be probed against their own tenant authority;
|
|
476
|
+
# redeeming their refresh token against the default tenant returns invalid_grant.
|
|
434
477
|
settings = get_settings()
|
|
435
|
-
|
|
478
|
+
if active_identity.external_entra_id and active_identity.tenant_id:
|
|
479
|
+
client = MSALClient.for_tenant(active_identity.tenant_id)
|
|
480
|
+
else:
|
|
481
|
+
client = MSALClient()
|
|
436
482
|
|
|
437
483
|
resources = {
|
|
438
484
|
"datex_api": {
|
|
@@ -525,6 +571,7 @@ def status(ctx: DxsContext) -> None:
|
|
|
525
571
|
"expires_in_seconds": expires_in_seconds,
|
|
526
572
|
"expires_in_human": format_expiration(expires_in_seconds),
|
|
527
573
|
},
|
|
574
|
+
"refresh_token": _refresh_token_info(active_identity, now),
|
|
528
575
|
},
|
|
529
576
|
"tokens": tokens_info if tokens_info else None,
|
|
530
577
|
}
|
|
@@ -109,19 +109,41 @@ def _launch_server(port: int, no_browser: bool, dev: bool, replace: bool = False
|
|
|
109
109
|
uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning", lifespan="on")
|
|
110
110
|
|
|
111
111
|
|
|
112
|
-
def
|
|
113
|
-
"""
|
|
112
|
+
def _resolve_server_port(probe_port: int) -> int:
|
|
113
|
+
"""Return the port of a running studio server, preferring the lockfile.
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
Falls back to probing the well-known port so a server started in another
|
|
116
|
+
shell or session is still found even when this process can't read or trust
|
|
117
|
+
the lockfile — e.g. a sandboxed Claude Code agent shell on Windows whose
|
|
118
|
+
PID-liveness check or ``Path.home()`` resolves differently than the user's
|
|
119
|
+
interactive session (issue #38).
|
|
117
120
|
|
|
118
121
|
Raises:
|
|
119
|
-
click.ClickException: If no running studio server
|
|
122
|
+
click.ClickException: If no running studio server can be located.
|
|
120
123
|
"""
|
|
121
124
|
lock = read_studio_lock()
|
|
122
|
-
if lock is None:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
if lock is not None:
|
|
126
|
+
return int(lock["port"])
|
|
127
|
+
if _port_in_use(probe_port):
|
|
128
|
+
return probe_port
|
|
129
|
+
raise click.ClickException("No studio server running. Start with: dxs studio")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _unreachable_error(subcommand: str, port: int, file: str | None, exc: Exception) -> str:
|
|
133
|
+
"""Build an actionable error for a server that is found but unreachable.
|
|
134
|
+
|
|
135
|
+
Distinguishes "no server" (handled in :func:`_resolve_server_port`) from
|
|
136
|
+
"found but the request failed" — typically a sandboxed agent shell that
|
|
137
|
+
can't open a loopback connection to the user's server.
|
|
138
|
+
"""
|
|
139
|
+
arg = f" {file}" if file else ""
|
|
140
|
+
return (
|
|
141
|
+
f"Found a studio server on port {port} but couldn't reach it: {exc}\n"
|
|
142
|
+
"If Studio is running in your own terminal and this is a sandboxed agent "
|
|
143
|
+
"shell (common on Windows), the agent process may be blocked from the "
|
|
144
|
+
"local server. Run the command in your own session instead:\n"
|
|
145
|
+
f" ! dxs studio {subcommand}{arg}"
|
|
146
|
+
)
|
|
125
147
|
|
|
126
148
|
|
|
127
149
|
@click.group(invoke_without_command=True)
|
|
@@ -166,18 +188,24 @@ def studio(ctx: click.Context, port: int, no_browser: bool, dev: bool, replace:
|
|
|
166
188
|
|
|
167
189
|
@studio.command("open")
|
|
168
190
|
@click.argument("file", type=click.Path())
|
|
169
|
-
|
|
191
|
+
@click.option(
|
|
192
|
+
"--port",
|
|
193
|
+
"-p",
|
|
194
|
+
type=int,
|
|
195
|
+
default=5051,
|
|
196
|
+
help="Port to probe when no studio lockfile is present (default: 5051)",
|
|
197
|
+
)
|
|
198
|
+
def studio_open(file: str, port: int) -> None:
|
|
170
199
|
"""Open a report file in the studio design canvas.
|
|
171
200
|
|
|
172
201
|
FILE is the path to a report file (.rdlx-json) to open.
|
|
173
202
|
"""
|
|
174
|
-
|
|
175
|
-
port = lock["port"]
|
|
203
|
+
server_port = _resolve_server_port(port)
|
|
176
204
|
resolved = str(Path(file).resolve())
|
|
177
205
|
|
|
178
206
|
try:
|
|
179
207
|
resp = httpx.post(
|
|
180
|
-
f"http://127.0.0.1:{
|
|
208
|
+
f"http://127.0.0.1:{server_port}/api/design/open",
|
|
181
209
|
json={"path": resolved},
|
|
182
210
|
timeout=10.0,
|
|
183
211
|
)
|
|
@@ -190,31 +218,69 @@ def studio_open(file: str) -> None:
|
|
|
190
218
|
raise click.ClickException(detail)
|
|
191
219
|
resp.raise_for_status()
|
|
192
220
|
except httpx.HTTPError as exc:
|
|
193
|
-
raise click.ClickException(
|
|
221
|
+
raise click.ClickException(_unreachable_error("open", server_port, file, exc)) from exc
|
|
194
222
|
|
|
195
223
|
data = resp.json()
|
|
196
224
|
file_path = data.get("path", resolved)
|
|
197
|
-
click.echo(f"Opened {file_path} in studio (http://127.0.0.1:{
|
|
225
|
+
click.echo(f"Opened {file_path} in studio (http://127.0.0.1:{server_port}/design)")
|
|
198
226
|
|
|
199
227
|
|
|
200
228
|
@studio.command("close")
|
|
201
|
-
|
|
229
|
+
@click.option(
|
|
230
|
+
"--port",
|
|
231
|
+
"-p",
|
|
232
|
+
type=int,
|
|
233
|
+
default=5051,
|
|
234
|
+
help="Port to probe when no studio lockfile is present (default: 5051)",
|
|
235
|
+
)
|
|
236
|
+
def studio_close(port: int) -> None:
|
|
202
237
|
"""Close the current design session in studio."""
|
|
203
|
-
|
|
204
|
-
port = lock["port"]
|
|
238
|
+
server_port = _resolve_server_port(port)
|
|
205
239
|
|
|
206
240
|
try:
|
|
207
241
|
resp = httpx.post(
|
|
208
|
-
f"http://127.0.0.1:{
|
|
242
|
+
f"http://127.0.0.1:{server_port}/api/design/close",
|
|
209
243
|
timeout=10.0,
|
|
210
244
|
)
|
|
211
245
|
resp.raise_for_status()
|
|
212
246
|
except httpx.HTTPError as exc:
|
|
213
|
-
raise click.ClickException(
|
|
247
|
+
raise click.ClickException(_unreachable_error("close", server_port, None, exc)) from exc
|
|
214
248
|
|
|
215
249
|
click.echo("Design session closed")
|
|
216
250
|
|
|
217
251
|
|
|
252
|
+
@studio.command("stop")
|
|
253
|
+
@click.option(
|
|
254
|
+
"--port",
|
|
255
|
+
"-p",
|
|
256
|
+
type=int,
|
|
257
|
+
default=5051,
|
|
258
|
+
help="Port to probe when no studio lockfile is present (default: 5051)",
|
|
259
|
+
)
|
|
260
|
+
def studio_stop(port: int) -> None:
|
|
261
|
+
"""Stop the studio server tracked by the lockfile.
|
|
262
|
+
|
|
263
|
+
Only terminates a server whose PID this CLI recorded in its own lockfile;
|
|
264
|
+
a process we don't manage is never killed.
|
|
265
|
+
"""
|
|
266
|
+
lock = read_studio_lock()
|
|
267
|
+
if lock is None:
|
|
268
|
+
if _port_in_use(port):
|
|
269
|
+
raise click.ClickException(
|
|
270
|
+
f"A server is running on port {port} but it isn't tracked by a "
|
|
271
|
+
"studio lockfile, so it won't be stopped automatically. Stop it "
|
|
272
|
+
"from the terminal that launched it."
|
|
273
|
+
)
|
|
274
|
+
raise click.ClickException("No studio server running.")
|
|
275
|
+
|
|
276
|
+
pid = int(lock["pid"])
|
|
277
|
+
server_port = int(lock["port"])
|
|
278
|
+
_terminate_pid(pid)
|
|
279
|
+
_await_port_free(server_port)
|
|
280
|
+
remove_studio_lock()
|
|
281
|
+
click.echo(f"Stopped studio (pid {pid}) on port {server_port}.")
|
|
282
|
+
|
|
283
|
+
|
|
218
284
|
@studio.command("status")
|
|
219
285
|
@click.option(
|
|
220
286
|
"--port",
|
|
@@ -9,13 +9,21 @@ from collections.abc import Callable
|
|
|
9
9
|
from datetime import datetime, timezone
|
|
10
10
|
from typing import Any, TypeVar
|
|
11
11
|
|
|
12
|
-
from dxs.core.auth.token_cache import CachedIdentity, MultiIdentityTokenCache
|
|
12
|
+
from dxs.core.auth.token_cache import CachedIdentity, MultiIdentityTokenCache, RefreshResult
|
|
13
13
|
from dxs.utils.errors import AuthenticationError
|
|
14
14
|
|
|
15
15
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
|
+
|
|
20
|
+
def _refresh_failed_message(outcome: RefreshResult) -> str:
|
|
21
|
+
"""Build an error message that includes the underlying refresh failure reason."""
|
|
22
|
+
base = "Authentication token expired and refresh failed"
|
|
23
|
+
detail = outcome.error_description or outcome.error
|
|
24
|
+
return f"{base}: {detail}" if detail else base
|
|
25
|
+
|
|
26
|
+
|
|
19
27
|
# Module-level cache for refreshed tokens (avoids repeated refresh calls)
|
|
20
28
|
_env_token_cache: dict[str, tuple[str, datetime]] = {}
|
|
21
29
|
|
|
@@ -167,18 +175,22 @@ def require_auth(f: F) -> F:
|
|
|
167
175
|
|
|
168
176
|
if token is None:
|
|
169
177
|
# Token expired or missing - try to refresh
|
|
170
|
-
|
|
171
|
-
if
|
|
178
|
+
outcome = cache.try_refresh_detailed()
|
|
179
|
+
if outcome.access_token is not None:
|
|
172
180
|
# Refresh succeeded, continue
|
|
173
181
|
return f(*args, **kwargs)
|
|
174
182
|
|
|
175
|
-
# Refresh failed - raise appropriate error
|
|
183
|
+
# Refresh failed - raise appropriate error, surfacing the real reason
|
|
176
184
|
identity = cache.get_active_identity()
|
|
177
185
|
if identity is not None:
|
|
178
186
|
raise AuthenticationError(
|
|
179
|
-
message=
|
|
187
|
+
message=_refresh_failed_message(outcome),
|
|
180
188
|
code="DXS-AUTH-001",
|
|
181
|
-
details={
|
|
189
|
+
details={
|
|
190
|
+
"expired_at": identity.expires_at.isoformat(),
|
|
191
|
+
"refresh_error": outcome.error,
|
|
192
|
+
"refresh_error_description": outcome.error_description,
|
|
193
|
+
},
|
|
182
194
|
suggestions=[
|
|
183
195
|
"Run 'dxs auth login' to re-authenticate",
|
|
184
196
|
],
|
|
@@ -238,16 +250,20 @@ def get_access_token() -> str:
|
|
|
238
250
|
return token
|
|
239
251
|
|
|
240
252
|
# Token expired or missing - try to refresh
|
|
241
|
-
|
|
242
|
-
if
|
|
243
|
-
return
|
|
253
|
+
outcome = cache.try_refresh_detailed()
|
|
254
|
+
if outcome.access_token is not None:
|
|
255
|
+
return outcome.access_token
|
|
244
256
|
|
|
245
|
-
# Refresh failed - raise appropriate error
|
|
257
|
+
# Refresh failed - raise appropriate error, surfacing the real reason
|
|
246
258
|
identity = cache.get_active_identity()
|
|
247
259
|
if identity is not None:
|
|
248
260
|
raise AuthenticationError(
|
|
249
|
-
message=
|
|
261
|
+
message=_refresh_failed_message(outcome),
|
|
250
262
|
code="DXS-AUTH-001",
|
|
263
|
+
details={
|
|
264
|
+
"refresh_error": outcome.error,
|
|
265
|
+
"refresh_error_description": outcome.error_description,
|
|
266
|
+
},
|
|
251
267
|
suggestions=["Run 'dxs auth login' to re-authenticate"],
|
|
252
268
|
)
|
|
253
269
|
else:
|
|
@@ -330,6 +346,12 @@ def get_access_token_for_scopes(scopes: list[str]) -> str:
|
|
|
330
346
|
expires_in = result.get("expires_in", 3600)
|
|
331
347
|
_env_token_cache[cache_key] = (token, datetime.now(timezone.utc))
|
|
332
348
|
file_cache.save_resource_token(cache_key, token, expires_in)
|
|
349
|
+
# Persist the rotated refresh token so the stored token stays fresh.
|
|
350
|
+
# Only for default-tenant identities: this client uses the default
|
|
351
|
+
# authority, so a rotated token is only valid for such identities.
|
|
352
|
+
new_rt = result.get("refresh_token")
|
|
353
|
+
if new_rt and not identity.external_entra_id:
|
|
354
|
+
file_cache.update_refresh_token(new_rt)
|
|
333
355
|
return token
|
|
334
356
|
except Exception as e:
|
|
335
357
|
logger.debug("Failed to acquire token for scopes %s: %s", scopes, e)
|
|
@@ -384,24 +384,52 @@ class MSALClient:
|
|
|
384
384
|
|
|
385
385
|
return None
|
|
386
386
|
|
|
387
|
-
def
|
|
388
|
-
|
|
387
|
+
def refresh_access_token_detailed(
|
|
388
|
+
self, refresh_token: str, scopes: list[str] | None = None
|
|
389
|
+
) -> dict[str, Any]:
|
|
390
|
+
"""Refresh an access token and return the raw MSAL result.
|
|
391
|
+
|
|
392
|
+
Unlike :meth:`refresh_access_token`, this never returns ``None`` and never
|
|
393
|
+
raises: on success the dict contains ``access_token`` (and usually a rotated
|
|
394
|
+
``refresh_token``); on failure it contains ``error`` and ``error_description``
|
|
395
|
+
(e.g. the underlying ``AADSTS`` code). Callers can surface that reason instead
|
|
396
|
+
of a generic "refresh failed".
|
|
389
397
|
|
|
390
398
|
Args:
|
|
391
399
|
refresh_token: The refresh token from a previous authentication.
|
|
400
|
+
scopes: Scopes to request. Defaults to the client's configured scopes.
|
|
392
401
|
|
|
393
402
|
Returns:
|
|
394
|
-
|
|
403
|
+
The MSAL token response dict, or a synthetic error dict.
|
|
395
404
|
"""
|
|
396
405
|
try:
|
|
397
406
|
result = self._app.acquire_token_by_refresh_token(
|
|
398
407
|
refresh_token=refresh_token,
|
|
399
|
-
scopes=self._scopes,
|
|
408
|
+
scopes=scopes or self._scopes,
|
|
400
409
|
)
|
|
401
|
-
if result and "access_token" in result:
|
|
402
|
-
return cast(dict[str, Any], result)
|
|
403
410
|
except Exception as e:
|
|
404
411
|
logger.debug("Failed to refresh access token: %s", e)
|
|
412
|
+
return {"error": "exception", "error_description": str(e)}
|
|
413
|
+
|
|
414
|
+
if result:
|
|
415
|
+
return cast(dict[str, Any], result)
|
|
416
|
+
return {
|
|
417
|
+
"error": "no_result",
|
|
418
|
+
"error_description": "No response received from the token endpoint.",
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
def refresh_access_token(self, refresh_token: str) -> dict[str, Any] | None:
|
|
422
|
+
"""Refresh access token using refresh token grant.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
refresh_token: The refresh token from a previous authentication.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Token response dict if successful, None if refresh failed.
|
|
429
|
+
"""
|
|
430
|
+
result = self.refresh_access_token_detailed(refresh_token)
|
|
431
|
+
if "access_token" in result:
|
|
432
|
+
return result
|
|
405
433
|
return None
|
|
406
434
|
|
|
407
435
|
def get_accounts(self) -> list[dict[str, Any]]:
|