|
| 1 | +# (C) 2022 GoodData Corporation |
| 2 | +from __future__ import annotations |
| 3 | + |
| 4 | +import functools |
| 5 | +from pathlib import Path |
| 6 | +from typing import List, Optional, Union |
| 7 | + |
| 8 | +import gooddata_afm_client.apis as afm_apis |
| 9 | +import gooddata_afm_client.models as afm_models |
| 10 | +from gooddata_sdk.catalog.catalog_service_base import CatalogServiceBase |
| 11 | +from gooddata_sdk.catalog.data_source.validation.data_source import DataSourceValidator |
| 12 | +from gooddata_sdk.catalog.types import ValidObjects |
| 13 | +from gooddata_sdk.catalog.workspace.declarative_model.workspace.analytics_model.analytics_model import ( |
| 14 | + CatalogDeclarativeAnalytics, |
| 15 | +) |
| 16 | +from gooddata_sdk.catalog.workspace.declarative_model.workspace.logical_model.ldm import CatalogDeclarativeModel |
| 17 | +from gooddata_sdk.catalog.workspace.declarative_model.workspace.workspace import LAYOUT_WORKSPACES_DIR |
| 18 | +from gooddata_sdk.catalog.workspace.entity_model.content_objects.dataset import ( |
| 19 | + CatalogAttribute, |
| 20 | + CatalogFact, |
| 21 | + CatalogLabel, |
| 22 | +) |
| 23 | +from gooddata_sdk.catalog.workspace.entity_model.content_objects.metric import CatalogMetric |
| 24 | +from gooddata_sdk.catalog.workspace.entity_model.graph_objects.graph import ( |
| 25 | + CatalogDependentEntitiesRequest, |
| 26 | + CatalogDependentEntitiesResponse, |
| 27 | +) |
| 28 | +from gooddata_sdk.catalog.workspace.model_container import CatalogWorkspaceContent |
| 29 | +from gooddata_sdk.client import GoodDataApiClient |
| 30 | +from gooddata_sdk.compute.model.attribute import Attribute |
| 31 | +from gooddata_sdk.compute.model.execution import ExecutionDefinition, compute_model_to_api_model |
| 32 | +from gooddata_sdk.compute.model.filter import Filter |
| 33 | +from gooddata_sdk.compute.model.metric import Metric |
| 34 | +from gooddata_sdk.utils import load_all_entities |
| 35 | + |
| 36 | +ValidObjectTypes = Union[Attribute, Metric, Filter, CatalogLabel, CatalogFact, CatalogMetric] |
| 37 | + |
| 38 | +# Use typing collection types to support python < py3.9 |
| 39 | +ValidObjectsInputType = Union[ValidObjectTypes, List[ValidObjectTypes], ExecutionDefinition] |
| 40 | + |
| 41 | + |
| 42 | +class CatalogWorkspaceContentService(CatalogServiceBase): |
| 43 | + # Note on the disabled checking: |
| 44 | + # generated client has issues parsing the vis objects; .. have to avoid return type checks |
| 45 | + # |
| 46 | + # note: the parsing is done lazily so it does not necessarily bomb on the next line but when trying to |
| 47 | + # access returned object's properties |
| 48 | + |
| 49 | + def __init__(self, api_client: GoodDataApiClient) -> None: |
| 50 | + super(CatalogWorkspaceContentService, self).__init__(api_client) |
| 51 | + self._afm_actions_api = afm_apis.ActionsApi(api_client.afm_client) |
| 52 | + |
| 53 | + def get_attributes_catalog(self, workspace_id: str) -> list[CatalogAttribute]: |
| 54 | + get_attributes = functools.partial( |
| 55 | + self._entities_api.get_all_entities_attributes, |
| 56 | + workspace_id, |
| 57 | + _check_return_type=False, |
| 58 | + ) |
| 59 | + attributes = load_all_entities(get_attributes) |
| 60 | + # Empty labels list is set. It will be changed in the future. |
| 61 | + catalog_attributes = [CatalogAttribute(attribute, []) for attribute in attributes.data] |
| 62 | + return catalog_attributes |
| 63 | + |
| 64 | + def get_labels_catalog(self, workspace_id: str) -> list[CatalogLabel]: |
| 65 | + get_labels = functools.partial( |
| 66 | + self._entities_api.get_all_entities_labels, |
| 67 | + workspace_id, |
| 68 | + _check_return_type=False, |
| 69 | + ) |
| 70 | + labels = load_all_entities(get_labels) |
| 71 | + catalog_labels = [CatalogLabel(label) for label in labels.data] |
| 72 | + return catalog_labels |
| 73 | + |
| 74 | + def get_metrics_catalog(self, workspace_id: str) -> list[CatalogMetric]: |
| 75 | + get_metrics = functools.partial( |
| 76 | + self._entities_api.get_all_entities_metrics, workspace_id, _check_return_type=False |
| 77 | + ) |
| 78 | + metrics = load_all_entities(get_metrics) |
| 79 | + catalog_metrics = [CatalogMetric(metric) for metric in metrics.data] |
| 80 | + return catalog_metrics |
| 81 | + |
| 82 | + def get_facts_catalog(self, workspace_id: str) -> list[CatalogFact]: |
| 83 | + get_facts = functools.partial(self._entities_api.get_all_entities_facts, workspace_id, _check_return_type=False) |
| 84 | + facts = load_all_entities(get_facts) |
| 85 | + catalog_facts = [CatalogFact(fact) for fact in facts.data] |
| 86 | + return catalog_facts |
| 87 | + |
| 88 | + def get_full_catalog(self, workspace_id: str) -> CatalogWorkspaceContent: |
| 89 | + """ |
| 90 | + Retrieves catalog for a workspace. Catalog contains all data sets and metrics defined in that workspace. |
| 91 | +
|
| 92 | + :param workspace_id: workspace identifier |
| 93 | + """ |
| 94 | + get_datasets = functools.partial( |
| 95 | + self._entities_api.get_all_entities_datasets, |
| 96 | + workspace_id, |
| 97 | + include=["attributes", "facts"], |
| 98 | + _check_return_type=False, |
| 99 | + ) |
| 100 | + |
| 101 | + get_attributes = functools.partial( |
| 102 | + self._entities_api.get_all_entities_attributes, |
| 103 | + workspace_id, |
| 104 | + include=["labels"], |
| 105 | + _check_return_type=False, |
| 106 | + ) |
| 107 | + |
| 108 | + get_metrics = functools.partial( |
| 109 | + self._entities_api.get_all_entities_metrics, workspace_id, _check_return_type=False |
| 110 | + ) |
| 111 | + |
| 112 | + attributes = load_all_entities(get_attributes) |
| 113 | + datasets = load_all_entities(get_datasets) |
| 114 | + metrics = load_all_entities(get_metrics) |
| 115 | + |
| 116 | + valid_obj_fun = functools.partial(self.compute_valid_objects, workspace_id) |
| 117 | + |
| 118 | + return CatalogWorkspaceContent.create_workspace_content_catalog(valid_obj_fun, datasets, attributes, metrics) |
| 119 | + |
| 120 | + @staticmethod |
| 121 | + def _prepare_afm_for_availability(items: list[ValidObjectTypes]) -> afm_models.AFM: |
| 122 | + attributes = [] |
| 123 | + metrics = [] |
| 124 | + filters = [] |
| 125 | + |
| 126 | + for item in items: |
| 127 | + if isinstance(item, Attribute): |
| 128 | + attributes.append(item) |
| 129 | + elif isinstance(item, Metric): |
| 130 | + metrics.append(item) |
| 131 | + elif isinstance(item, Filter): |
| 132 | + filters.append(item) |
| 133 | + elif isinstance(item, CatalogLabel): |
| 134 | + attributes.append(item.as_computable()) |
| 135 | + elif isinstance(item, (CatalogFact, CatalogMetric)): |
| 136 | + metrics.append(item.as_computable()) |
| 137 | + |
| 138 | + return compute_model_to_api_model(attributes=attributes, metrics=metrics, filters=filters) |
| 139 | + |
| 140 | + def compute_valid_objects(self, workspace_id: str, ctx: ValidObjectsInputType) -> ValidObjects: |
| 141 | + """ |
| 142 | + Returns attributes, facts, and metrics which are valid to add to a context that already |
| 143 | + contains some entities from the semantic model. The entities are typically used to compute analytics and |
| 144 | + come from the execution definition. You may, however, specify the entities through different layers of |
| 145 | + convenience. |
| 146 | +
|
| 147 | + :param workspace_id: workspace identifier |
| 148 | + :param ctx: items already in context. you can specify context in one of the following ways: |
| 149 | +
|
| 150 | + - single item or list of items from the execution model |
| 151 | + - single item or list of items from catalog model; catalog fact, label or metric may be added |
| 152 | + - the entire execution definition that is used to compute analytics |
| 153 | +
|
| 154 | + :return: a dict of sets; type of available object is used as key in the dict, |
| 155 | + the value is a set containing id's of available items |
| 156 | + """ |
| 157 | + if isinstance(ctx, ExecutionDefinition): |
| 158 | + afm = compute_model_to_api_model(attributes=ctx.attributes, metrics=ctx.metrics, filters=ctx.filters) |
| 159 | + else: |
| 160 | + _ctx = ctx if isinstance(ctx, list) else [ctx] |
| 161 | + afm = self._prepare_afm_for_availability(_ctx) |
| 162 | + |
| 163 | + query = afm_models.AfmValidObjectsQuery(afm=afm, types=["facts", "attributes", "measures"]) |
| 164 | + response = self._afm_actions_api.compute_valid_objects(workspace_id=workspace_id, afm_valid_objects_query=query) |
| 165 | + |
| 166 | + by_type: dict[str, set[str]] = dict() |
| 167 | + |
| 168 | + for available in response.items: |
| 169 | + _type = available["type"] |
| 170 | + |
| 171 | + if _type not in by_type: |
| 172 | + items_of_type: set[str] = set() |
| 173 | + by_type[_type] = items_of_type |
| 174 | + else: |
| 175 | + items_of_type = by_type[_type] |
| 176 | + |
| 177 | + items_of_type.add(available["id"]) |
| 178 | + |
| 179 | + return by_type |
| 180 | + |
| 181 | + def get_dependent_entities_graph(self, workspace_id: str) -> CatalogDependentEntitiesResponse: |
| 182 | + return CatalogDependentEntitiesResponse.from_api( |
| 183 | + self._metadata_actions_api.get_dependent_entities_graph(workspace_id=workspace_id) |
| 184 | + ) |
| 185 | + |
| 186 | + def get_dependent_entities_graph_from_entry_points( |
| 187 | + self, workspace_id: str, dependent_entities_request: CatalogDependentEntitiesRequest |
| 188 | + ) -> CatalogDependentEntitiesResponse: |
| 189 | + return CatalogDependentEntitiesResponse.from_api( |
| 190 | + self._metadata_actions_api.get_dependent_entities_graph_from_entry_points( |
| 191 | + workspace_id=workspace_id, dependent_entities_request=dependent_entities_request.to_api() |
| 192 | + ) |
| 193 | + ) |
| 194 | + |
| 195 | + # Declarative methods for workspace content service are listed below |
| 196 | + |
| 197 | + def get_declarative_ldm(self, workspace_id: str) -> CatalogDeclarativeModel: |
| 198 | + return CatalogDeclarativeModel.from_api(self._layout_api.get_logical_model(workspace_id)) |
| 199 | + |
| 200 | + def store_declarative_ldm(self, workspace_id: str, layout_root_path: Path = Path.cwd()) -> None: |
| 201 | + workspace_folder = self.layout_workspace_folder(workspace_id, layout_root_path) |
| 202 | + self.get_declarative_ldm(workspace_id).store_to_disk(workspace_folder) |
| 203 | + |
| 204 | + def layout_workspace_folder(self, workspace_id: str, layout_root_path: Path) -> Path: |
| 205 | + return self.layout_organization_folder(layout_root_path) / LAYOUT_WORKSPACES_DIR / workspace_id |
| 206 | + |
| 207 | + def load_declarative_ldm(self, workspace_id: str, layout_root_path: Path = Path.cwd()) -> CatalogDeclarativeModel: |
| 208 | + workspace_folder = self.layout_workspace_folder(workspace_id, layout_root_path) |
| 209 | + return CatalogDeclarativeModel.load_from_disk(workspace_folder) |
| 210 | + |
| 211 | + def load_and_put_declarative_ldm( |
| 212 | + self, |
| 213 | + workspace_id: str, |
| 214 | + layout_root_path: Path = Path.cwd(), |
| 215 | + validator: Optional[DataSourceValidator] = None, |
| 216 | + ) -> None: |
| 217 | + declarative_ldm = self.load_declarative_ldm(workspace_id, layout_root_path) |
| 218 | + self.put_declarative_ldm(workspace_id, declarative_ldm, validator) |
| 219 | + |
| 220 | + def put_declarative_ldm( |
| 221 | + self, workspace_id: str, ldm: CatalogDeclarativeModel, validator: Optional[DataSourceValidator] = None |
| 222 | + ) -> None: |
| 223 | + if validator is not None: |
| 224 | + validator.validate_ldm(ldm) |
| 225 | + self._layout_api.set_logical_model(workspace_id, ldm.to_api()) |
| 226 | + |
| 227 | + def get_declarative_analytics_model(self, workspace_id: str) -> CatalogDeclarativeAnalytics: |
| 228 | + return CatalogDeclarativeAnalytics.from_api(self._layout_api.get_analytics_model(workspace_id)) |
| 229 | + |
| 230 | + def put_declarative_analytics_model(self, workspace_id: str, analytics_model: CatalogDeclarativeAnalytics) -> None: |
| 231 | + self._layout_api.set_analytics_model(workspace_id, analytics_model.to_api()) |
| 232 | + |
| 233 | + def store_declarative_analytics_model(self, workspace_id: str, layout_root_path: Path = Path.cwd()) -> None: |
| 234 | + workspace_folder = self.layout_workspace_folder(workspace_id, layout_root_path) |
| 235 | + declarative_analytics_model = self.get_declarative_analytics_model(workspace_id) |
| 236 | + declarative_analytics_model.store_to_disk(workspace_folder) |
| 237 | + |
| 238 | + def load_declarative_analytics_model( |
| 239 | + self, workspace_id: str, layout_root_path: Path = Path.cwd() |
| 240 | + ) -> CatalogDeclarativeAnalytics: |
| 241 | + workspace_folder = self.layout_workspace_folder(workspace_id, layout_root_path) |
| 242 | + return CatalogDeclarativeAnalytics.load_from_disk(workspace_folder) |
| 243 | + |
| 244 | + def load_and_put_declarative_analytics_model(self, workspace_id: str, layout_root_path: Path = Path.cwd()) -> None: |
| 245 | + declarative_analytics_model = self.load_declarative_analytics_model(workspace_id, layout_root_path) |
| 246 | + self.put_declarative_analytics_model(workspace_id, declarative_analytics_model) |
0 commit comments