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.
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/PKG-INFO +1 -1
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/pyproject.toml +1 -1
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/__init__.py +1 -1
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/auth.py +31 -12
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/datasource.py +31 -157
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/report.py +235 -19
- datex_studio_cli-0.3.1/src/dxs/core/datasource/parsers.py +140 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/datasource_binding.py +55 -1
- datex_studio_cli-0.3.1/src/dxs/report/owned_datasource.py +367 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/wrapper.py +3 -1
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/.gitignore +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/README.md +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/report-creator.skill +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/__main__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/cli.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/api.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/branch.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/config.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/crm.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/devops.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/document.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/env.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/explore.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/marketplace.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/odata.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/organization.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/proxy.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/release_notes.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/repo.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/schema.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/servicepack.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/source.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/studio.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/commands/user.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/context.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/api/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/api/client.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/api/endpoints.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/api/metadata_models.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/api/models.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/auth/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/auth/decorators.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/auth/msal_client.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/auth/token_cache.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/cache.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/datasource/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/datasource/generator.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/datasource/models.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/datasource/validator.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/devops/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/devops/client.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/devops/models.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/dynamics/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/dynamics/client.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/footprint/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/footprint/client.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/footprint/edmx_parser.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/footprint/metadata.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/graph.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/output/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/output/csv_fmt.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/output/formatter.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/output/json_fmt.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/output/yaml_fmt.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/release_notes.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/core/responses.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/models/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/engine.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/field_parser.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/models.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/preview.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/schema.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/templates/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/report/validator.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/click_options.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/config.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/errors.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/filtering.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/paths.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/resolvers.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/responses.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/restricted.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/utils/sorting.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/__init__.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/app.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/proxy_app.py +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/404.html +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/RIG3nV-yzLdq0OlVxUM6i/_buildManifest.js +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/RIG3nV-yzLdq0OlVxUM6i/_ssgManifest.js +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/255-67e8754147461423.js +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/841-8775e8c75ff8c949.js +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/871-370fd7261c4ea9bc.js +0 -0
- {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
- {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
- {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
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/main-1a0dcce460eb61ce.js +0 -0
- {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
- {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
- {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
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/_next/static/css/686bbface7277f83.css +0 -0
- {datex_studio_cli-0.3.0 → datex_studio_cli-0.3.1}/src/dxs/web/static/index.html +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
672
|
+
# Construct admin consent URLs for both apps
|
|
673
673
|
settings = get_settings()
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
|
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
|
-
"
|
|
690
|
-
"instructions":
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
707
|
-
"instructions":
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 =
|
|
482
|
-
config.queryOptionsObjectTypeDef
|
|
483
|
-
)
|
|
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 =
|
|
496
|
-
config.queryOptionsObjectTypeDef
|
|
497
|
-
)
|
|
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(
|
|
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
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
-
|
|
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:
|