datex-studio-cli 0.4.8__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.8 → datex_studio_cli-0.4.10}/PKG-INFO +1 -1
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/pyproject.toml +1 -1
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/__init__.py +1 -1
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/auth.py +51 -4
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/source.py +186 -109
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/studio.py +85 -19
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/__init__.py +2 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/app_config.py +90 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/auth/decorators.py +33 -11
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/auth/msal_client.py +34 -6
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/auth/token_cache.py +204 -59
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/client.py +5 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/release_notes.py +196 -1
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/paths.py +33 -11
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/app.py +3 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/.gitignore +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/README.md +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/report-creator.skill +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/scripts/analyze_corpus.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/scripts/generate_rdlx_schema.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/__main__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/cli.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/api.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/branch.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/config.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/configuration.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/crm.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/datasource.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/devops.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/document.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/endpoint.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/env.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/explore.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/function.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/marketplace.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/odata.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/organization.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/proxy.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/release_notes.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/repo.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/add_cmds.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/api.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/batch.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/create.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/data.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/dataset.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/edit.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/folder.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/preview.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/report/schema_cmds.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/schema.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/servicepack.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/telemetry.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/commands/user.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/context.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/client.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/endpoints.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/metadata_models.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/api/models.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/auth/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/cache.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/flow_generator.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/flow_models.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/generator.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/models.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/parsers.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/resolver.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/type_def_parser.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/datasource/validator.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/designer/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/designer/flow_models.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/designer/models.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/designer/parsers.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/devops/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/devops/client.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/devops/helpers.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/devops/models.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/dynamics/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/dynamics/client.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/dynamics/metadata_cache.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/edmx_parser.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/footprint/metadata.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/function/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/function/generator.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/function/models.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/function/validator.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/graph.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/csv_fmt.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/formatter.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/json_fmt.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/redaction.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/output/yaml_fmt.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/responses.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/config.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/identity.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/recorder.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/scrubber.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/core/telemetry/sender.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/models/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/datasource_binding.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/engine.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/field_parser.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/folder.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/manifest.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/models.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/preview.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/preview_arjs.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/schema.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/svg_renderer.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/templates/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/validate_arjs.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/validator.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/report/wrapper.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/click_options.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/config.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/errors.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/filtering.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/image.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/resolvers.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/responses.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/restricted.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/sorting.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/utils/update_check.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/__init__.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/design.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/proxy_app.py +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/404/index.html +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/404.html +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
- {datex_studio_cli-0.4.8 → 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.8 → 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.8 → 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.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/capture/index.html +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/capture/index.txt +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/index.html +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/design/index.txt +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/index.html +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/index.txt +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/render-cli.js +0 -0
- {datex_studio_cli-0.4.8 → datex_studio_cli-0.4.10}/src/dxs/web/static/report-preview.html +0 -0
- {datex_studio_cli-0.4.8 → 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
|
}
|
|
@@ -22,6 +22,7 @@ from dxs.core.api import (
|
|
|
22
22
|
RepoEndpoints,
|
|
23
23
|
SourceControlEndpoints,
|
|
24
24
|
WorkitemEndpoints,
|
|
25
|
+
diff_references,
|
|
25
26
|
fetch_references,
|
|
26
27
|
fetch_settings,
|
|
27
28
|
)
|
|
@@ -29,10 +30,13 @@ from dxs.core.auth import require_auth
|
|
|
29
30
|
from dxs.core.output.yaml_fmt import LiteralString
|
|
30
31
|
from dxs.core.release_notes import (
|
|
31
32
|
aggregate_contributors,
|
|
33
|
+
build_repo_index,
|
|
32
34
|
classify_dependencies,
|
|
33
35
|
collect_workitems_batch,
|
|
34
36
|
enrich_workitems_with_devops_data,
|
|
35
37
|
is_sync_commit,
|
|
38
|
+
resolve_changed_dependencies,
|
|
39
|
+
resolve_root_org_ids,
|
|
36
40
|
)
|
|
37
41
|
from dxs.utils.errors import ValidationError
|
|
38
42
|
from dxs.utils.responses import list_response, single
|
|
@@ -2770,106 +2774,186 @@ def deps_diff(ctx: DxsContext, from_branch: int, to_branch: int) -> None:
|
|
|
2770
2774
|
"""
|
|
2771
2775
|
client = ApiClient()
|
|
2772
2776
|
|
|
2773
|
-
# Fetch dependencies for both branches in parallel conceptually
|
|
2774
2777
|
ctx.log(f"Fetching dependencies for branch {from_branch}...")
|
|
2775
2778
|
from_deps = fetch_references(client, from_branch, direct_only=True)
|
|
2776
2779
|
|
|
2777
2780
|
ctx.log(f"Fetching dependencies for branch {to_branch}...")
|
|
2778
2781
|
to_deps = fetch_references(client, to_branch, direct_only=True)
|
|
2779
2782
|
|
|
2780
|
-
#
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
to_deps = [to_deps] if to_deps else []
|
|
2785
|
-
|
|
2786
|
-
# Build lookup maps by referenceName
|
|
2787
|
-
from_by_ref = {d.get("referenceName"): d for d in from_deps}
|
|
2788
|
-
to_by_ref = {d.get("referenceName"): d for d in to_deps}
|
|
2789
|
-
|
|
2790
|
-
# Compute differences
|
|
2791
|
-
added = []
|
|
2792
|
-
removed = []
|
|
2793
|
-
updated = []
|
|
2794
|
-
unchanged_count = 0
|
|
2795
|
-
|
|
2796
|
-
# Find added and updated
|
|
2797
|
-
for ref_name, to_dep in to_by_ref.items():
|
|
2798
|
-
if ref_name not in from_by_ref:
|
|
2799
|
-
# New dependency
|
|
2800
|
-
added.append(
|
|
2801
|
-
{
|
|
2802
|
-
"referenceName": ref_name,
|
|
2803
|
-
"name": to_dep.get("name"),
|
|
2804
|
-
"description": to_dep.get("description"),
|
|
2805
|
-
"versionName": to_dep.get("versionName"),
|
|
2806
|
-
"versionId": to_dep.get("marketPlaceApplicationVersionId"),
|
|
2807
|
-
"applicationId": to_dep.get("applicationId"),
|
|
2808
|
-
"commitTitle": to_dep.get("commitTitle"),
|
|
2809
|
-
"isDirect": to_dep.get("isDirect"),
|
|
2810
|
-
}
|
|
2811
|
-
)
|
|
2812
|
-
else:
|
|
2813
|
-
from_dep = from_by_ref[ref_name]
|
|
2814
|
-
from_version_id = from_dep.get("marketPlaceApplicationVersionId")
|
|
2815
|
-
to_version_id = to_dep.get("marketPlaceApplicationVersionId")
|
|
2783
|
+
# Diff by resolved `version` keyed on uniqueIdentifier. (The legacy
|
|
2784
|
+
# marketPlaceApplicationVersionId field no longer exists in the AppConfig
|
|
2785
|
+
# payload, so the previous comparison reported everything as unchanged.)
|
|
2786
|
+
diff = diff_references(from_deps, to_deps)
|
|
2816
2787
|
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
"fromApplicationId": from_dep.get("applicationId"),
|
|
2826
|
-
"toApplicationId": to_dep.get("applicationId"),
|
|
2827
|
-
"fromVersionName": from_dep.get("versionName"),
|
|
2828
|
-
"toVersionName": to_dep.get("versionName"),
|
|
2829
|
-
"fromCommitTitle": from_dep.get("commitTitle"),
|
|
2830
|
-
"toCommitTitle": to_dep.get("commitTitle"),
|
|
2831
|
-
"isDirect": to_dep.get("isDirect"),
|
|
2832
|
-
}
|
|
2833
|
-
)
|
|
2834
|
-
else:
|
|
2835
|
-
unchanged_count += 1
|
|
2788
|
+
ctx.output(
|
|
2789
|
+
single(
|
|
2790
|
+
item=diff,
|
|
2791
|
+
semantic_key="dependency_changes",
|
|
2792
|
+
from_branch_id=from_branch,
|
|
2793
|
+
to_branch_id=to_branch,
|
|
2794
|
+
)
|
|
2795
|
+
)
|
|
2836
2796
|
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2797
|
+
|
|
2798
|
+
@source.command("release-tree")
|
|
2799
|
+
@click.option(
|
|
2800
|
+
"--from",
|
|
2801
|
+
"from_branch",
|
|
2802
|
+
type=int,
|
|
2803
|
+
required=True,
|
|
2804
|
+
help="Source branch ID (earlier release of the root application)",
|
|
2805
|
+
)
|
|
2806
|
+
@click.option(
|
|
2807
|
+
"--to",
|
|
2808
|
+
"to_branch",
|
|
2809
|
+
type=int,
|
|
2810
|
+
required=True,
|
|
2811
|
+
help="Target branch ID (later release of the root application)",
|
|
2812
|
+
)
|
|
2813
|
+
@click.option(
|
|
2814
|
+
"--recursive",
|
|
2815
|
+
is_flag=True,
|
|
2816
|
+
default=False,
|
|
2817
|
+
help="Also walk changed dependencies of changed dependencies (transitive)",
|
|
2818
|
+
)
|
|
2819
|
+
@click.option(
|
|
2820
|
+
"--max-depth",
|
|
2821
|
+
type=int,
|
|
2822
|
+
default=10,
|
|
2823
|
+
help="Maximum recursion depth when --recursive is set (default: 10)",
|
|
2824
|
+
)
|
|
2825
|
+
@pass_context
|
|
2826
|
+
@require_auth
|
|
2827
|
+
def release_tree(
|
|
2828
|
+
ctx: DxsContext,
|
|
2829
|
+
from_branch: int,
|
|
2830
|
+
to_branch: int,
|
|
2831
|
+
recursive: bool,
|
|
2832
|
+
max_depth: int,
|
|
2833
|
+
) -> None:
|
|
2834
|
+
"""Build the complete changed-dependency worklist between two releases.
|
|
2835
|
+
|
|
2836
|
+
Diffs the root application's dependencies by resolved ``version``, then
|
|
2837
|
+
resolves every changed dependency to its from/to branch IDs (the IDs needed
|
|
2838
|
+
to ``compare`` or drill each package). This is the enumeration step for
|
|
2839
|
+
release notes: one call yields the full list of (package, from_branch,
|
|
2840
|
+
to_branch) tuples to analyze.
|
|
2841
|
+
|
|
2842
|
+
Unlike ``deps-diff`` (which lists version deltas only), this resolves branch
|
|
2843
|
+
IDs. With ``--recursive`` it walks changed dependencies of changed
|
|
2844
|
+
dependencies; for a flat aggregator app (e.g. FootprintManager, which
|
|
2845
|
+
declares every package directly) a single pass is already complete.
|
|
2846
|
+
|
|
2847
|
+
\b
|
|
2848
|
+
Options:
|
|
2849
|
+
--from ID Earlier release branch ID of the root app
|
|
2850
|
+
--to ID Later release branch ID of the root app
|
|
2851
|
+
--recursive Walk transitive changed dependencies
|
|
2852
|
+
--max-depth N Recursion cap with --recursive (default: 10)
|
|
2853
|
+
|
|
2854
|
+
\b
|
|
2855
|
+
Examples:
|
|
2856
|
+
dxs source release-tree --from 73496 --to 82962
|
|
2857
|
+
dxs source release-tree --from 73496 --to 82962 --recursive
|
|
2858
|
+
"""
|
|
2859
|
+
if from_branch == to_branch:
|
|
2860
|
+
raise ValidationError(
|
|
2861
|
+
message="Cannot compare a branch to itself",
|
|
2862
|
+
code="DXS-VAL-002",
|
|
2863
|
+
suggestions=["Provide different --from and --to branch IDs"],
|
|
2864
|
+
)
|
|
2865
|
+
|
|
2866
|
+
client = ApiClient()
|
|
2867
|
+
|
|
2868
|
+
# Root metadata (reference name + version names for the worklist header).
|
|
2869
|
+
from_data = client.get(BranchEndpoints.get(from_branch))
|
|
2870
|
+
to_data = client.get(BranchEndpoints.get(to_branch))
|
|
2871
|
+
|
|
2872
|
+
# The root may be a customer application (e.g. tob-footprint) that references
|
|
2873
|
+
# packages from its own org alongside the shared Datex catalog. Index that
|
|
2874
|
+
# org too, or those packages (e.g. tob-reports) silently fail to resolve.
|
|
2875
|
+
extra_org_ids = resolve_root_org_ids(client, to_data, from_data)
|
|
2876
|
+
root_name = to_data.get("referenceName") or to_data.get("name")
|
|
2877
|
+
|
|
2878
|
+
root = {
|
|
2879
|
+
"reference_name": root_name,
|
|
2880
|
+
"from_version": from_data.get("versionName"),
|
|
2881
|
+
"to_version": to_data.get("versionName"),
|
|
2882
|
+
"from_branch_id": from_branch,
|
|
2883
|
+
"to_branch_id": to_branch,
|
|
2884
|
+
"depth": 0,
|
|
2885
|
+
"is_root": True,
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
# Repo index is built once and reused across all levels.
|
|
2889
|
+
ctx.log("Building repository index...")
|
|
2890
|
+
repo_index = build_repo_index(client, extra_org_ids=extra_org_ids)
|
|
2891
|
+
|
|
2892
|
+
dependencies: list[dict[str, Any]] = []
|
|
2893
|
+
added: list[dict[str, Any]] = []
|
|
2894
|
+
removed: list[dict[str, Any]] = []
|
|
2895
|
+
# visited keyed by the work unit, so the same (package, from, to) is resolved once.
|
|
2896
|
+
visited: set[tuple[Any, Any, Any]] = set()
|
|
2897
|
+
|
|
2898
|
+
# BFS queue of (from_branch_id, to_branch_id, depth) to diff.
|
|
2899
|
+
queue: list[tuple[int, int, int]] = [(from_branch, to_branch, 1)]
|
|
2900
|
+
while queue:
|
|
2901
|
+
cur_from, cur_to, depth = queue.pop(0)
|
|
2902
|
+
ctx.log(f"Diffing dependencies at depth {depth} ({cur_from} -> {cur_to})...")
|
|
2903
|
+
try:
|
|
2904
|
+
from_refs = fetch_references(client, cur_from, direct_only=True)
|
|
2905
|
+
to_refs = fetch_references(client, cur_to, direct_only=True)
|
|
2906
|
+
except Exception:
|
|
2907
|
+
logger.debug("Failed to fetch references for %s/%s", cur_from, cur_to, exc_info=True)
|
|
2908
|
+
continue
|
|
2909
|
+
|
|
2910
|
+
diff = diff_references(from_refs, to_refs)
|
|
2911
|
+
if depth == 1:
|
|
2912
|
+
added = diff["added"]
|
|
2913
|
+
removed = diff["removed"]
|
|
2914
|
+
|
|
2915
|
+
resolved = resolve_changed_dependencies(client, diff["updated"], repo_index=repo_index)
|
|
2916
|
+
for entry in resolved:
|
|
2917
|
+
key = (
|
|
2918
|
+
entry.get("unique_identifier"),
|
|
2919
|
+
entry.get("from_version"),
|
|
2920
|
+
entry.get("to_version"),
|
|
2850
2921
|
)
|
|
2922
|
+
if key in visited:
|
|
2923
|
+
continue
|
|
2924
|
+
visited.add(key)
|
|
2925
|
+
entry["depth"] = depth
|
|
2926
|
+
dependencies.append(entry)
|
|
2927
|
+
|
|
2928
|
+
if (
|
|
2929
|
+
recursive
|
|
2930
|
+
and depth < max_depth
|
|
2931
|
+
and entry.get("from_branch_id")
|
|
2932
|
+
and entry.get("to_branch_id")
|
|
2933
|
+
):
|
|
2934
|
+
queue.append((entry["from_branch_id"], entry["to_branch_id"], depth + 1))
|
|
2851
2935
|
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
removed.sort(key=lambda x: str(x.get("referenceName") or ""))
|
|
2855
|
-
updated.sort(key=lambda x: str(x.get("referenceName") or ""))
|
|
2936
|
+
dependencies.sort(key=lambda d: (d.get("depth", 0), str(d.get("reference_name") or "")))
|
|
2937
|
+
resolved_ok = sum(1 for d in dependencies if not d.get("resolve_error"))
|
|
2856
2938
|
|
|
2857
2939
|
ctx.output(
|
|
2858
2940
|
single(
|
|
2859
2941
|
item={
|
|
2942
|
+
"root": root,
|
|
2860
2943
|
"summary": {
|
|
2944
|
+
"dependencies_total": len(dependencies),
|
|
2945
|
+
"resolved_ok": resolved_ok,
|
|
2946
|
+
"resolve_failed": len(dependencies) - resolved_ok,
|
|
2861
2947
|
"added_count": len(added),
|
|
2862
2948
|
"removed_count": len(removed),
|
|
2863
|
-
"
|
|
2864
|
-
"
|
|
2865
|
-
"from_total": len(from_deps),
|
|
2866
|
-
"to_total": len(to_deps),
|
|
2949
|
+
"max_depth_reached": max((d.get("depth", 0) for d in dependencies), default=0),
|
|
2950
|
+
"recursive": recursive,
|
|
2867
2951
|
},
|
|
2952
|
+
"dependencies": dependencies,
|
|
2868
2953
|
"added": added,
|
|
2869
2954
|
"removed": removed,
|
|
2870
|
-
"updated": updated,
|
|
2871
2955
|
},
|
|
2872
|
-
semantic_key="
|
|
2956
|
+
semantic_key="release_tree",
|
|
2873
2957
|
from_branch_id=from_branch,
|
|
2874
2958
|
to_branch_id=to_branch,
|
|
2875
2959
|
)
|
|
@@ -3661,54 +3745,47 @@ def compare(
|
|
|
3661
3745
|
filtered.append(entry)
|
|
3662
3746
|
committed_branches_list = filtered
|
|
3663
3747
|
|
|
3664
|
-
# Get dependency changes
|
|
3748
|
+
# Get dependency changes. Diff by resolved `version` keyed on uniqueIdentifier
|
|
3749
|
+
# (the legacy marketPlaceApplicationVersionId field is gone from the AppConfig
|
|
3750
|
+
# payload, which is why the old comparison always reported zero changes).
|
|
3665
3751
|
dep_changes: dict[str, list[Any]] = {"added": [], "removed": [], "updated": []}
|
|
3666
3752
|
ctx.log("Comparing dependencies...")
|
|
3667
3753
|
try:
|
|
3668
3754
|
from_deps = fetch_references(client, from_branch, direct_only=True)
|
|
3669
3755
|
to_deps = fetch_references(client, to_branch, direct_only=True)
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
# Find added, removed, and updated
|
|
3676
|
-
for ref, dep in to_by_ref.items():
|
|
3677
|
-
if ref not in from_by_ref:
|
|
3678
|
-
dep_changes["added"].append(dep)
|
|
3679
|
-
else:
|
|
3680
|
-
from_dep = from_by_ref[ref]
|
|
3681
|
-
from_dep_ver = from_dep.get("marketPlaceApplicationVersionId")
|
|
3682
|
-
to_dep_ver = dep.get("marketPlaceApplicationVersionId")
|
|
3683
|
-
if from_dep_ver != to_dep_ver:
|
|
3684
|
-
dep_changes["updated"].append(
|
|
3685
|
-
{
|
|
3686
|
-
"reference_name": ref,
|
|
3687
|
-
"from_branch_id": from_dep.get("applicationId"),
|
|
3688
|
-
"to_branch_id": dep.get("applicationId"),
|
|
3689
|
-
}
|
|
3690
|
-
)
|
|
3691
|
-
|
|
3692
|
-
for ref, dep in from_by_ref.items():
|
|
3693
|
-
if ref not in to_by_ref:
|
|
3694
|
-
dep_changes["removed"].append(dep)
|
|
3756
|
+
diff = diff_references(from_deps, to_deps)
|
|
3757
|
+
dep_changes["added"] = diff["added"]
|
|
3758
|
+
dep_changes["removed"] = diff["removed"]
|
|
3759
|
+
dep_changes["updated"] = diff["updated"]
|
|
3695
3760
|
except Exception:
|
|
3696
3761
|
logger.debug("Failed to fetch dependencies for comparison", exc_info=True)
|
|
3697
3762
|
|
|
3698
|
-
# Phase 4: Recursive dependency enrichment
|
|
3763
|
+
# Phase 4: Recursive dependency enrichment. The AppConfig payload carries no
|
|
3764
|
+
# branch IDs, so resolve each changed dep's from/to branch IDs before drilling.
|
|
3699
3765
|
sync_only_dep_count = 0
|
|
3700
3766
|
if recursive and dep_changes["updated"]:
|
|
3767
|
+
ctx.log("Resolving dependency branch IDs...")
|
|
3768
|
+
# Index the root app's own org too, so a customer app's packages resolve.
|
|
3769
|
+
extra_org_ids = resolve_root_org_ids(client, to_branch_data, from_branch_data)
|
|
3770
|
+
resolved = resolve_changed_dependencies(
|
|
3771
|
+
client, dep_changes["updated"], extra_org_ids=extra_org_ids
|
|
3772
|
+
)
|
|
3773
|
+
drillable = [d for d in resolved if d.get("from_branch_id") and d.get("to_branch_id")]
|
|
3774
|
+
unresolved = [
|
|
3775
|
+
d for d in resolved if not (d.get("from_branch_id") and d.get("to_branch_id"))
|
|
3776
|
+
]
|
|
3777
|
+
|
|
3701
3778
|
ctx.log("Recursively comparing updated dependencies...")
|
|
3702
|
-
meaningful_deps, sync_only_deps = classify_dependencies(client,
|
|
3779
|
+
meaningful_deps, sync_only_deps = classify_dependencies(client, drillable)
|
|
3703
3780
|
sync_only_dep_count = len(sync_only_deps)
|
|
3704
3781
|
|
|
3705
3782
|
if exclude_sync:
|
|
3706
3783
|
# When both flags are on, separate sync-only deps
|
|
3707
|
-
dep_changes["updated"] = meaningful_deps
|
|
3784
|
+
dep_changes["updated"] = meaningful_deps + unresolved
|
|
3708
3785
|
dep_changes["sync_only"] = sync_only_deps
|
|
3709
3786
|
else:
|
|
3710
3787
|
# Keep all but mark them
|
|
3711
|
-
dep_changes["updated"] = meaningful_deps + sync_only_deps
|
|
3788
|
+
dep_changes["updated"] = meaningful_deps + sync_only_deps + unresolved
|
|
3712
3789
|
|
|
3713
3790
|
# Phase 2: Collect work item IDs from all committed branches + target branch
|
|
3714
3791
|
workitem_ids: set[str] = set()
|
|
@@ -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",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""API module for Datex Studio HTTP client and models."""
|
|
2
2
|
|
|
3
3
|
from dxs.core.api.app_config import (
|
|
4
|
+
diff_references,
|
|
4
5
|
fetch_app_config,
|
|
5
6
|
fetch_references,
|
|
6
7
|
fetch_settings,
|
|
@@ -43,6 +44,7 @@ from dxs.core.api.models import (
|
|
|
43
44
|
__all__ = [
|
|
44
45
|
"ApiClient",
|
|
45
46
|
# AppConfig helpers
|
|
47
|
+
"diff_references",
|
|
46
48
|
"fetch_app_config",
|
|
47
49
|
"fetch_references",
|
|
48
50
|
"fetch_settings",
|