datex-studio-cli 0.3.0__tar.gz → 0.3.1__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.
Files changed (112) hide show
  1. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/PKG-INFO +1 -1
  2. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/pyproject.toml +1 -1
  3. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/__init__.py +1 -1
  4. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/auth.py +31 -12
  5. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/datasource.py +31 -157
  6. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/report.py +235 -19
  7. datex_studio_cli-0.3.1/src/dxs/core/datasource/parsers.py +140 -0
  8. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/datasource_binding.py +55 -1
  9. datex_studio_cli-0.3.1/src/dxs/report/owned_datasource.py +367 -0
  10. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/wrapper.py +3 -1
  11. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/.gitignore +0 -0
  12. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/README.md +0 -0
  13. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/report-creator.skill +0 -0
  14. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/__main__.py +0 -0
  15. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/cli.py +0 -0
  16. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/__init__.py +0 -0
  17. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/api.py +0 -0
  18. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/branch.py +0 -0
  19. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/config.py +0 -0
  20. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/crm.py +0 -0
  21. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/devops.py +0 -0
  22. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/document.py +0 -0
  23. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/env.py +0 -0
  24. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/explore.py +0 -0
  25. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/marketplace.py +0 -0
  26. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/odata.py +0 -0
  27. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/organization.py +0 -0
  28. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/proxy.py +0 -0
  29. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/release_notes.py +0 -0
  30. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/repo.py +0 -0
  31. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/schema.py +0 -0
  32. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/servicepack.py +0 -0
  33. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/source.py +0 -0
  34. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/studio.py +0 -0
  35. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/user.py +0 -0
  36. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/context.py +0 -0
  37. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/__init__.py +0 -0
  38. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/api/__init__.py +0 -0
  39. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/api/client.py +0 -0
  40. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/api/endpoints.py +0 -0
  41. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/api/metadata_models.py +0 -0
  42. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/api/models.py +0 -0
  43. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/auth/__init__.py +0 -0
  44. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/auth/decorators.py +0 -0
  45. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/auth/msal_client.py +0 -0
  46. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/auth/token_cache.py +0 -0
  47. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/cache.py +0 -0
  48. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/datasource/__init__.py +0 -0
  49. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/datasource/generator.py +0 -0
  50. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/datasource/models.py +0 -0
  51. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/datasource/validator.py +0 -0
  52. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/devops/__init__.py +0 -0
  53. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/devops/client.py +0 -0
  54. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/devops/models.py +0 -0
  55. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/dynamics/__init__.py +0 -0
  56. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/dynamics/client.py +0 -0
  57. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/footprint/__init__.py +0 -0
  58. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/footprint/client.py +0 -0
  59. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/footprint/edmx_parser.py +0 -0
  60. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/footprint/metadata.py +0 -0
  61. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/graph.py +0 -0
  62. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/output/__init__.py +0 -0
  63. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/output/csv_fmt.py +0 -0
  64. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/output/formatter.py +0 -0
  65. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/output/json_fmt.py +0 -0
  66. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/output/yaml_fmt.py +0 -0
  67. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/release_notes.py +0 -0
  68. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/responses.py +0 -0
  69. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/models/__init__.py +0 -0
  70. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/__init__.py +0 -0
  71. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/engine.py +0 -0
  72. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/field_parser.py +0 -0
  73. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/models.py +0 -0
  74. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/preview.py +0 -0
  75. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/schema.py +0 -0
  76. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/templates/__init__.py +0 -0
  77. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
  78. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
  79. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/validator.py +0 -0
  80. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/__init__.py +0 -0
  81. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/click_options.py +0 -0
  82. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/config.py +0 -0
  83. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/errors.py +0 -0
  84. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/filtering.py +0 -0
  85. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/paths.py +0 -0
  86. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/resolvers.py +0 -0
  87. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/responses.py +0 -0
  88. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/restricted.py +0 -0
  89. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/sorting.py +0 -0
  90. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/__init__.py +0 -0
  91. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/app.py +0 -0
  92. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/proxy_app.py +0 -0
  93. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/404.html +0 -0
  94. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/RIG3nV-yzLdq0OlVxUM6i/_buildManifest.js +0 -0
  95. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/RIG3nV-yzLdq0OlVxUM6i/_ssgManifest.js +0 -0
  96. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/255-67e8754147461423.js +0 -0
  97. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
  98. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/841-8775e8c75ff8c949.js +0 -0
  99. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/871-370fd7261c4ea9bc.js +0 -0
  100. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
  101. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/app/layout-fb92e6e3144d6d3a.js +0 -0
  102. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/app/page-b5e545d0d4880f6f.js +0 -0
  103. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
  104. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/main-1a0dcce460eb61ce.js +0 -0
  105. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
  106. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
  107. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
  108. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
  109. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
  110. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/css/686bbface7277f83.css +0 -0
  111. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/index.html +0 -0
  112. {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/index.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datex-studio-cli
3
- Version: 0.3.0
3
+ Version: 0.3.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datex-studio-cli"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "CLI for Datex Studio low-code platform, designed for LLM-based AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,4 +1,4 @@
1
1
  """Datex Studio CLI - Command-line interface for Datex Studio platform."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.1"
4
4
  __app_name__ = "dxs"
@@ -669,42 +669,61 @@ def consent(ctx: DxsContext, org: str, open: bool) -> None:
669
669
  )
670
670
  raise SystemExit(1)
671
671
 
672
- # Construct admin consent URL
672
+ # Construct admin consent URLs for both apps
673
673
  settings = get_settings()
674
- consent_url = (
675
- f"https://login.microsoftonline.com/{org_info.tenant_id}/adminconsent"
676
- f"?client_id={settings.azure_client_id}"
677
- )
674
+ base_consent = f"https://login.microsoftonline.com/{org_info.tenant_id}/adminconsent"
675
+
676
+ datex_consent_url = f"{base_consent}?client_id={settings.azure_client_id}"
677
+
678
+ # Extract Footprint API client ID from scope (api://{client_id}/access_as_user)
679
+ fp_scope = settings.footprint_scope
680
+ fp_client_id = fp_scope.split("://")[1].split("/")[0] if "://" in fp_scope else None
681
+ footprint_consent_url = f"{base_consent}?client_id={fp_client_id}" if fp_client_id else None
682
+
683
+ consent_apps = [
684
+ {"name": "Datex Studio API", "client_id": settings.azure_client_id, "url": datex_consent_url},
685
+ ]
686
+ if footprint_consent_url and fp_client_id:
687
+ consent_apps.append(
688
+ {"name": "Footprint API", "client_id": fp_client_id, "url": footprint_consent_url}
689
+ )
678
690
 
679
691
  if open:
680
692
  ctx.output(
681
693
  single(
682
694
  item={
683
695
  "status": "opening_browser",
684
- "message": f"Opening admin consent page for {org_info.name}",
696
+ "message": f"Opening admin consent pages for {org_info.name}",
685
697
  "organization": org_info.name,
686
698
  "organization_id": org_info.id,
687
699
  "tenant_id": org_info.tenant_id,
688
700
  "domain": org_info.external_entra_id_domain_name,
689
- "consent_url": consent_url,
690
- "instructions": "A tenant admin must complete the consent flow in the browser",
701
+ "consent_apps": consent_apps,
702
+ "instructions": (
703
+ "A tenant admin must complete the consent flow for BOTH apps. "
704
+ "Two browser tabs will open — approve each one."
705
+ ),
691
706
  },
692
707
  semantic_key="consent",
693
708
  )
694
709
  )
695
- webbrowser.open(consent_url)
710
+ for app in consent_apps:
711
+ webbrowser.open(app["url"])
696
712
  else:
697
713
  ctx.output(
698
714
  single(
699
715
  item={
700
716
  "status": "url_generated",
701
- "message": f"Admin consent URL for {org_info.name}",
717
+ "message": f"Admin consent URLs for {org_info.name}",
702
718
  "organization": org_info.name,
703
719
  "organization_id": org_info.id,
704
720
  "tenant_id": org_info.tenant_id,
705
721
  "domain": org_info.external_entra_id_domain_name,
706
- "consent_url": consent_url,
707
- "instructions": "Have a tenant admin open this URL to grant consent",
722
+ "consent_apps": consent_apps,
723
+ "instructions": (
724
+ "Have a tenant admin open BOTH URLs to grant consent. "
725
+ "Both apps must be approved for full CLI functionality."
726
+ ),
708
727
  },
709
728
  semantic_key="consent",
710
729
  )
@@ -13,101 +13,36 @@ from dxs.core.api.client import ApiClient
13
13
  from dxs.core.api.endpoints import ConfigurationEndpoints
14
14
  from dxs.core.auth import require_auth
15
15
  from dxs.core.datasource import DatasourceGenerator
16
- from dxs.core.datasource.models import (
17
- DatasourceConfig,
18
- DSDynamicOrderByConfig,
19
- DSLinkedDatasourceConfig,
20
- DSLinkedDatasourceConfigRef,
21
- DSValueConfig,
22
- TypeConfig,
16
+ from dxs.core.datasource.models import DatasourceConfig
17
+ from dxs.core.datasource.parsers import (
18
+ auto_populate_dynamic_filters as _auto_populate_dynamic_filters,
19
+ )
20
+ from dxs.core.datasource.parsers import (
21
+ auto_populate_dynamic_orderbys as _auto_populate_dynamic_orderbys,
22
+ )
23
+ from dxs.core.datasource.parsers import (
24
+ parse_custom_column as _parse_custom_column,
25
+ )
26
+ from dxs.core.datasource.parsers import (
27
+ parse_dynamic_filter as _parse_dynamic_filter,
28
+ )
29
+ from dxs.core.datasource.parsers import (
30
+ parse_dynamic_orderby as _parse_dynamic_orderby,
31
+ )
32
+ from dxs.core.datasource.parsers import (
33
+ parse_linked as _parse_linked,
34
+ )
35
+ from dxs.core.datasource.parsers import (
36
+ parse_linked_skip as _parse_linked_skip,
37
+ )
38
+ from dxs.core.datasource.parsers import (
39
+ parse_linked_top as _parse_linked_top,
23
40
  )
24
41
  from dxs.core.datasource.validator import DatasourceValidator
25
42
  from dxs.utils.errors import ApiError, ValidationError
26
43
  from dxs.utils.responses import list_response, single
27
44
 
28
45
 
29
- def _parse_dynamic_filter(value: str) -> TypeConfig:
30
- """Parse 'property:type' → TypeConfig. Raises UsageError if malformed."""
31
- parts = value.split(":", 1)
32
- if len(parts) != 2:
33
- raise click.UsageError(f"--dynamic-filter must be 'property:type', got: {value!r}")
34
- prop, typ = parts
35
- valid_types = {"string", "number", "date", "boolean"}
36
- if typ not in valid_types:
37
- raise click.UsageError(f"--dynamic-filter type must be one of {valid_types}, got: {typ!r}")
38
- return TypeConfig(id=prop, type=typ) # type: ignore[arg-type]
39
-
40
-
41
- def _parse_dynamic_orderby(value: str) -> DSDynamicOrderByConfig:
42
- """Parse 'Property' or 'Owner.Name' → DSDynamicOrderByConfig."""
43
- return DSDynamicOrderByConfig(property=value.split("."))
44
-
45
-
46
- def _parse_linked(value: str) -> DSLinkedDatasourceConfig:
47
- """Parse linked datasource spec → DSLinkedDatasourceConfig.
48
-
49
- Format:
50
- oneToOne/oneToMany: 'name:type:target' (mergeByValue is always null for these types)
51
- oneToOneWithMerge: 'name:type:target:mergeBy' (mergeByValue required)
52
-
53
- A 4th component is accepted for all types for convenience but is silently
54
- discarded for oneToOne/oneToMany — the C# validator and Handlebars template
55
- do not use mergeByValue for those types (the UI always sets it to null).
56
- """
57
- parts = value.split(":", 3)
58
- if len(parts) < 3:
59
- raise click.UsageError(f"--linked must be 'name:type:target[:mergeBy]', got: {value!r}")
60
- name, typ = parts[0], parts[1]
61
- valid_types = {"oneToOne", "oneToMany", "oneToOneWithMerge"}
62
- if typ not in valid_types:
63
- raise click.UsageError(f"--linked type must be one of {valid_types}, got: {typ!r}")
64
- target = parts[2]
65
- if typ == "oneToOneWithMerge":
66
- if len(parts) < 4 or not parts[3]:
67
- raise click.UsageError(
68
- "--linked oneToOneWithMerge requires a mergeBy expression: 'name:type:target:mergeBy'"
69
- )
70
- merge_by: str | None = parts[3]
71
- else:
72
- # oneToOne / oneToMany: mergeByValue must be null — the template does not use it
73
- # and the C# validator type-checks any non-null value against the entity type.
74
- merge_by = None
75
- return DSLinkedDatasourceConfig(
76
- name=name,
77
- type=typ, # type: ignore[arg-type]
78
- datasourceConfig=DSLinkedDatasourceConfigRef(configId=target),
79
- mergeByValue=merge_by,
80
- )
81
-
82
-
83
- def _parse_custom_column(value: str) -> DSValueConfig:
84
- """Parse 'name:type:expression' → DSValueConfig (splits on first 2 colons)."""
85
- parts = value.split(":", 2)
86
- if len(parts) != 3:
87
- raise click.UsageError(f"--custom-column must be 'name:type:expression', got: {value!r}")
88
- name, typ, expression = parts
89
- valid_types = {"string", "number", "boolean", "date", "object"}
90
- if typ not in valid_types:
91
- raise click.UsageError(f"--custom-column type must be one of {valid_types}, got: {typ!r}")
92
- return DSValueConfig(name=name, type=typ, value=expression) # type: ignore[arg-type]
93
-
94
-
95
- def _parse_linked_top(value: str) -> tuple[str, str]:
96
- """Parse 'name:value' → (linked_name, top_value) for --linked-top."""
97
- parts = value.split(":", 1)
98
- if len(parts) != 2 or not parts[0] or not parts[1]:
99
- raise click.UsageError(f"--linked-top must be 'name:value', got: {value!r}")
100
- return parts[0], parts[1]
101
-
102
-
103
- def _parse_linked_skip(value: str) -> tuple[str, str]:
104
- """Parse 'name:value' → (linked_name, skip_value) for --linked-skip."""
105
- parts = value.split(":", 1)
106
- if len(parts) != 2 or not parts[0] or not parts[1]:
107
- raise click.UsageError(f"--linked-skip must be 'name:value', got: {value!r}")
108
- return parts[0], parts[1]
109
-
110
-
111
46
  def _build_upsert_summary(config: DatasourceConfig) -> dict:
112
47
  """Build a summary of the generated datasource configuration."""
113
48
  summary: dict = {"result_type": "single" if not config.isCollection else "collection"}
@@ -123,65 +58,6 @@ def _build_upsert_summary(config: DatasourceConfig) -> dict:
123
58
  return summary
124
59
 
125
60
 
126
- _DYNAMIC_FILTER_TYPES = {"boolean", "date", "number", "string"}
127
-
128
-
129
- def _auto_populate_dynamic_filters(
130
- type_def: list[TypeConfig] | None,
131
- ) -> list[TypeConfig]:
132
- """Auto-populate dynamic filters from queryOptionsObjectTypeDef.
133
-
134
- Mirrors the UI's getQueryOptionsSelectedAsDynamicFilter: includes primitive
135
- non-collection properties and recurses into non-collection object properties.
136
- """
137
- if not type_def:
138
- return []
139
- result: list[TypeConfig] = []
140
- for prop in type_def:
141
- if prop.type in _DYNAMIC_FILTER_TYPES and not prop.isCollection:
142
- result.append(TypeConfig(id=prop.id, type=prop.type))
143
- elif (
144
- prop.type == "object"
145
- and prop.objectTypeDef
146
- and not prop.isCollection
147
- ):
148
- nested = _auto_populate_dynamic_filters(prop.objectTypeDef)
149
- if nested:
150
- result.append(
151
- TypeConfig(id=prop.id, type="object", objectTypeDef=nested)
152
- )
153
- return result
154
-
155
-
156
- def _auto_populate_dynamic_orderbys(
157
- type_def: list[TypeConfig] | None,
158
- parent_property: list[str] | None = None,
159
- ) -> list[DSDynamicOrderByConfig]:
160
- """Auto-populate dynamic orderbys from queryOptionsObjectTypeDef.
161
-
162
- Mirrors the UI's getQueryOptionsSelectedAsDynamicOrderBy: includes primitive
163
- non-collection properties and recurses into non-collection object properties.
164
- """
165
- if not type_def:
166
- return []
167
- prefix = parent_property or []
168
- result: list[DSDynamicOrderByConfig] = []
169
- for prop in type_def:
170
- if prop.type in _DYNAMIC_FILTER_TYPES and not prop.isCollection:
171
- result.append(DSDynamicOrderByConfig(property=[*prefix, prop.id]))
172
- elif (
173
- prop.type == "object"
174
- and prop.objectTypeDef
175
- and not prop.isCollection
176
- ):
177
- result.extend(
178
- _auto_populate_dynamic_orderbys(
179
- prop.objectTypeDef, [*prefix, prop.id]
180
- )
181
- )
182
- return result
183
-
184
-
185
61
  @click.group()
186
62
  def datasource() -> None:
187
63
  """Datasource configuration generation and management.
@@ -478,9 +354,9 @@ def upsert(
478
354
  # Auto-populate dynamicFilters from queryOptionsObjectTypeDef if none specified,
479
355
  # matching the UI's allSelectedIsDynamicFiltersChanged() behavior.
480
356
  if not dynamic_filters:
481
- config.dynamicFilters = _auto_populate_dynamic_filters(
482
- config.queryOptionsObjectTypeDef
483
- ) or None
357
+ config.dynamicFilters = (
358
+ _auto_populate_dynamic_filters(config.queryOptionsObjectTypeDef) or None
359
+ )
484
360
  elif dynamic_filters:
485
361
  config.allSelectedIsDynamicFilters = False
486
362
 
@@ -492,9 +368,9 @@ def upsert(
492
368
  # Auto-populate dynamicOrderBys from queryOptionsObjectTypeDef if none specified,
493
369
  # matching the UI's allSelectedIsDynamicOrderBysChanged() behavior.
494
370
  if not dynamic_orderbys:
495
- config.dynamicOrderBys = _auto_populate_dynamic_orderbys(
496
- config.queryOptionsObjectTypeDef
497
- ) or None
371
+ config.dynamicOrderBys = (
372
+ _auto_populate_dynamic_orderbys(config.queryOptionsObjectTypeDef) or None
373
+ )
498
374
  elif dynamic_orderbys:
499
375
  config.allSelectedIsDynamicOrderBys = False
500
376
 
@@ -661,9 +537,7 @@ def delete(ctx: DxsContext, reference_name: str, branch: int | None) -> None:
661
537
  )
662
538
  config_id = existing["id"]
663
539
 
664
- api_client.delete(
665
- ConfigurationEndpoints.delete(branch_id, "datasource", config_id)
666
- )
540
+ api_client.delete(ConfigurationEndpoints.delete(branch_id, "datasource", config_id))
667
541
  ctx.output(
668
542
  single(
669
543
  item={
@@ -1235,7 +1235,9 @@ def validate(ctx: DxsContext, file: str) -> None:
1235
1235
  default=False,
1236
1236
  help="Separate PNG per page instead of single stitched image",
1237
1237
  )
1238
- @click.option("--width", type=int, default=None, help="Image width in pixels (default: match page at 96 DPI)")
1238
+ @click.option(
1239
+ "--width", type=int, default=None, help="Image width in pixels (default: match page at 96 DPI)"
1240
+ )
1239
1241
  @click.option("--timeout", type=int, default=30, help="Render timeout in seconds")
1240
1242
  @pass_context
1241
1243
  @handle_errors
@@ -1330,6 +1332,133 @@ def preview(
1330
1332
  "Format: ds_param=expression or Alias.ds_param=expression. Repeatable."
1331
1333
  ),
1332
1334
  )
1335
+ @click.option(
1336
+ "--owned-datasource",
1337
+ "owned_datasources",
1338
+ multiple=True,
1339
+ metavar="REF:ALIAS",
1340
+ help="Embed an owned datasource in the report. Format: ref_name:alias. Repeatable.",
1341
+ )
1342
+ @click.option(
1343
+ "--owned-connection",
1344
+ "owned_connections",
1345
+ multiple=True,
1346
+ type=int,
1347
+ metavar="ID",
1348
+ help="Connection ID for owned datasource (index-matched with --owned-datasource).",
1349
+ )
1350
+ @click.option(
1351
+ "--owned-query",
1352
+ "owned_queries",
1353
+ multiple=True,
1354
+ metavar="QUERY",
1355
+ help="OData query for owned datasource (index-matched).",
1356
+ )
1357
+ @click.option(
1358
+ "--owned-title",
1359
+ "owned_titles",
1360
+ multiple=True,
1361
+ metavar="TITLE",
1362
+ help="Title for owned datasource (index-matched).",
1363
+ )
1364
+ @click.option(
1365
+ "--owned-description",
1366
+ "owned_descriptions",
1367
+ multiple=True,
1368
+ metavar="DESC",
1369
+ help="Description for owned datasource (index-matched, optional).",
1370
+ )
1371
+ @click.option(
1372
+ "--owned-api-setting-name",
1373
+ "owned_api_setting_names",
1374
+ multiple=True,
1375
+ metavar="NAME",
1376
+ help="API setting name for owned datasource (index-matched, optional — auto-resolved).",
1377
+ )
1378
+ @click.option(
1379
+ "--owned-param-keys",
1380
+ "owned_param_keys",
1381
+ multiple=True,
1382
+ metavar="DS_NAME",
1383
+ help="Enable param-keys for named owned datasource.",
1384
+ )
1385
+ @click.option(
1386
+ "--owned-key-param-name",
1387
+ "owned_key_param_names",
1388
+ multiple=True,
1389
+ metavar="DS_NAME:VALUE",
1390
+ help="Custom key param name for named owned datasource.",
1391
+ )
1392
+ @click.option(
1393
+ "--owned-detect-params",
1394
+ "owned_detect_params",
1395
+ multiple=True,
1396
+ metavar="DS_NAME",
1397
+ help="Enable detect-params for named owned datasource.",
1398
+ )
1399
+ @click.option(
1400
+ "--owned-output-single",
1401
+ "owned_output_singles",
1402
+ multiple=True,
1403
+ metavar="DS_NAME",
1404
+ help="Enable output-single for named owned datasource.",
1405
+ )
1406
+ @click.option(
1407
+ "--owned-all-dynamic-filters",
1408
+ "owned_all_dynamic_filters",
1409
+ multiple=True,
1410
+ metavar="DS_NAME",
1411
+ help="Enable all-dynamic-filters for named owned datasource.",
1412
+ )
1413
+ @click.option(
1414
+ "--owned-all-dynamic-orderbys",
1415
+ "owned_all_dynamic_orderbys",
1416
+ multiple=True,
1417
+ metavar="DS_NAME",
1418
+ help="Enable all-dynamic-orderbys for named owned datasource.",
1419
+ )
1420
+ @click.option(
1421
+ "--owned-dynamic-filter",
1422
+ "owned_dynamic_filters",
1423
+ multiple=True,
1424
+ metavar="DS_NAME:PROP:TYPE",
1425
+ help="Add dynamic filter to named owned datasource. Repeatable.",
1426
+ )
1427
+ @click.option(
1428
+ "--owned-dynamic-orderby",
1429
+ "owned_dynamic_orderbys",
1430
+ multiple=True,
1431
+ metavar="DS_NAME:PROP",
1432
+ help="Add dynamic orderby to named owned datasource. Repeatable.",
1433
+ )
1434
+ @click.option(
1435
+ "--owned-linked",
1436
+ "owned_linkeds",
1437
+ multiple=True,
1438
+ metavar="DS_NAME:NAME:TYPE:TARGET[:MERGE]",
1439
+ help="Add linked datasource to named owned datasource. Repeatable.",
1440
+ )
1441
+ @click.option(
1442
+ "--owned-custom-column",
1443
+ "owned_custom_columns",
1444
+ multiple=True,
1445
+ metavar="DS_NAME:NAME:TYPE:EXPR",
1446
+ help="Add custom column to named owned datasource. Repeatable.",
1447
+ )
1448
+ @click.option(
1449
+ "--owned-linked-top",
1450
+ "owned_linked_tops",
1451
+ multiple=True,
1452
+ metavar="DS_NAME:LINKED:VALUE",
1453
+ help="Set top limit on linked within owned datasource. Repeatable.",
1454
+ )
1455
+ @click.option(
1456
+ "--owned-linked-skip",
1457
+ "owned_linked_skips",
1458
+ multiple=True,
1459
+ metavar="DS_NAME:LINKED:VALUE",
1460
+ help="Set skip offset on linked within owned datasource. Repeatable.",
1461
+ )
1333
1462
  @pass_context
1334
1463
  @require_auth
1335
1464
  @handle_errors
@@ -1344,6 +1473,24 @@ def upload(
1344
1473
  use_datasources: tuple[str, ...],
1345
1474
  params_raw: tuple[str, ...],
1346
1475
  datasource_params_raw: tuple[str, ...],
1476
+ owned_datasources: tuple[str, ...],
1477
+ owned_connections: tuple[int, ...],
1478
+ owned_queries: tuple[str, ...],
1479
+ owned_titles: tuple[str, ...],
1480
+ owned_descriptions: tuple[str, ...],
1481
+ owned_api_setting_names: tuple[str, ...],
1482
+ owned_param_keys: tuple[str, ...],
1483
+ owned_key_param_names: tuple[str, ...],
1484
+ owned_detect_params: tuple[str, ...],
1485
+ owned_output_singles: tuple[str, ...],
1486
+ owned_all_dynamic_filters: tuple[str, ...],
1487
+ owned_all_dynamic_orderbys: tuple[str, ...],
1488
+ owned_dynamic_filters: tuple[str, ...],
1489
+ owned_dynamic_orderbys: tuple[str, ...],
1490
+ owned_linkeds: tuple[str, ...],
1491
+ owned_custom_columns: tuple[str, ...],
1492
+ owned_linked_tops: tuple[str, ...],
1493
+ owned_linked_skips: tuple[str, ...],
1347
1494
  ) -> None:
1348
1495
  """Upload report to Datex Studio as a report configuration.
1349
1496
 
@@ -1412,13 +1559,16 @@ def upload(
1412
1559
  f"Ensure the datasource is upserted before uploading the report."
1413
1560
  ) from exc
1414
1561
  ds_json: dict = ds.get("json") or {}
1415
- type_def = ds_json.get("queryOptionsObjectTypeDef") or []
1416
- is_coll = ds_json.get("resultIsCollection", False)
1417
- config_out_parameters: list[dict] = (
1418
- [{"id": "result", "type": "object", "isCollection": is_coll, "objectTypeDef": type_def}]
1419
- if type_def
1420
- else []
1421
- )
1562
+ config_out_parameters: list[dict] = ds_json.get("outParams") or []
1563
+
1564
+ # Extract dynamic ordering/filtering and determine method name
1565
+ dynamic_order_bys = ds_json.get("dynamicOrderBys")
1566
+ dynamic_filters = ds_json.get("dynamicFilters")
1567
+ ds_type = ds_json.get("type", "oDataQuery")
1568
+ if ds_type == "oDataQuery":
1569
+ datasource_method_name = "get"
1570
+ else:
1571
+ datasource_method_name = "get" if ds_json.get("getFlow") else "getList"
1422
1572
 
1423
1573
  # Collect param type info from datasource inParams
1424
1574
  ds_in_params: list[dict] = ds_json.get("inParams") or []
@@ -1439,14 +1589,85 @@ def upload(
1439
1589
  param_types=param_types,
1440
1590
  param_required=param_required,
1441
1591
  key_def=ds_json.get("keyDef"),
1592
+ dynamic_order_bys=dynamic_order_bys,
1593
+ dynamic_filters=dynamic_filters,
1594
+ datasource_method_name=datasource_method_name,
1442
1595
  )
1443
1596
  datasource_configs.append(entry)
1444
1597
 
1598
+ # Process owned datasources
1599
+ owned_ds_configs: list[dict] = []
1600
+ if owned_datasources:
1601
+ from dxs.report.owned_datasource import (
1602
+ generate_owned_datasource,
1603
+ group_owned_specs,
1604
+ )
1605
+
1606
+ specs = group_owned_specs(
1607
+ owned_datasources=owned_datasources,
1608
+ owned_connections=owned_connections,
1609
+ owned_queries=owned_queries,
1610
+ owned_titles=owned_titles,
1611
+ owned_descriptions=owned_descriptions,
1612
+ owned_api_setting_names=owned_api_setting_names,
1613
+ owned_param_keys=owned_param_keys,
1614
+ owned_key_param_names=owned_key_param_names,
1615
+ owned_detect_params=owned_detect_params,
1616
+ owned_output_singles=owned_output_singles,
1617
+ owned_all_dynamic_filters=owned_all_dynamic_filters,
1618
+ owned_all_dynamic_orderbys=owned_all_dynamic_orderbys,
1619
+ owned_dynamic_filters=owned_dynamic_filters,
1620
+ owned_dynamic_orderbys=owned_dynamic_orderbys,
1621
+ owned_linkeds=owned_linkeds,
1622
+ owned_custom_columns=owned_custom_columns,
1623
+ owned_linked_tops=owned_linked_tops,
1624
+ owned_linked_skips=owned_linked_skips,
1625
+ )
1626
+
1627
+ for spec in specs:
1628
+ ctx.log(f"Generating owned datasource: {spec.reference_name}")
1629
+ ds_config, ds_ref = generate_owned_datasource(
1630
+ spec=spec,
1631
+ api_client=client,
1632
+ branch_id=branch_id,
1633
+ )
1634
+
1635
+ # Apply --datasource-param mappings for this owned DS
1636
+ ds_params_owned: list[tuple[str, str]] = []
1637
+ ds_params_owned.extend(ds_params_by_alias.get("", []))
1638
+ ds_params_owned.extend(ds_params_by_alias.get(spec.alias, []))
1639
+ if ds_params_owned:
1640
+ in_params_list = ds_config.get("inParams") or []
1641
+ pt = {p["id"]: p.get("type", "string") for p in in_params_list if "id" in p}
1642
+ pr = {p["id"]: p.get("required") for p in in_params_list if "id" in p}
1643
+ ds_ref["datasourceConfig"]["configParameters"] = [
1644
+ {
1645
+ "parameter": {
1646
+ "id": ds_param,
1647
+ "type": pt.get(ds_param, "string"),
1648
+ "required": pr.get(ds_param),
1649
+ },
1650
+ "value": expr,
1651
+ }
1652
+ for ds_param, expr in ds_params_owned
1653
+ ]
1654
+
1655
+ owned_ds_configs.append(ds_config)
1656
+ datasource_configs.append(ds_ref)
1657
+
1658
+ # Validate alias uniqueness across all datasources (external + owned)
1659
+ seen_aliases: set[str] = set()
1660
+ for dc in datasource_configs:
1661
+ alias = dc["alias"]
1662
+ if alias in seen_aliases:
1663
+ raise click.UsageError(
1664
+ f"Duplicate datasource alias: {alias!r}. "
1665
+ f"Each --use-datasource and --owned-datasource must have a unique alias."
1666
+ )
1667
+ seen_aliases.add(alias)
1668
+
1445
1669
  # Build inParams from --param flags
1446
- in_params = [
1447
- {"id": name, "type": ptype, "required": True}
1448
- for name, ptype in parsed_params
1449
- ]
1670
+ in_params = [{"id": name, "type": ptype, "required": True} for name, ptype in parsed_params]
1450
1671
 
1451
1672
  # Build Datex Studio wrapper
1452
1673
  wrapper = rdl_to_datex_wrapper(
@@ -1457,6 +1678,7 @@ def upload(
1457
1678
  report_type=report_type,
1458
1679
  datasource_configs=datasource_configs if datasource_configs else None,
1459
1680
  in_params=in_params if in_params else None,
1681
+ datasources=owned_ds_configs if owned_ds_configs else None,
1460
1682
  )
1461
1683
 
1462
1684
  # Upload via API — upsert using ConfigurationEndpoints
@@ -1643,13 +1865,7 @@ def datasource_fields(ctx: DxsContext, datasource_ref: str, branch: int | None)
1643
1865
  )
1644
1866
 
1645
1867
  ds_json: dict = ds.get("json") or {}
1646
- type_def = ds_json.get("queryOptionsObjectTypeDef") or []
1647
- is_coll = ds_json.get("resultIsCollection", False)
1648
- config_out_parameters: list[dict] = (
1649
- [{"id": "result", "type": "object", "isCollection": is_coll, "objectTypeDef": type_def}]
1650
- if type_def
1651
- else []
1652
- )
1868
+ config_out_parameters: list[dict] = ds_json.get("outParams") or []
1653
1869
  is_collection, flat_fields, collections = parse_fields(config_out_parameters)
1654
1870
 
1655
1871
  def _field_entry(f: ParsedField) -> dict: