gooddata-pipelines 1.48.1.dev4__py3-none-any.whl → 1.49.1.dev1__py3-none-any.whl

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.

Potentially problematic release.


This version of gooddata-pipelines might be problematic. Click here for more details.

@@ -1,103 +1,101 @@
1
1
  # (C) 2025 GoodData Corporation
2
- from dataclasses import dataclass
2
+ from abc import abstractmethod
3
3
  from enum import Enum
4
- from typing import Any, Iterator, TypeAlias
4
+ from typing import Any, Iterator, TypeAlias, TypeVar
5
5
 
6
+ import attrs
6
7
  from gooddata_sdk.catalog.identifier import CatalogAssigneeIdentifier
7
8
  from gooddata_sdk.catalog.permission.declarative_model.permission import (
8
9
  CatalogDeclarativeSingleWorkspacePermission,
9
10
  CatalogDeclarativeWorkspacePermissions,
10
11
  )
12
+ from pydantic import BaseModel
11
13
 
12
14
  from gooddata_pipelines.provisioning.utils.exceptions import BaseUserException
13
15
 
14
- # TODO: refactor the full load and incremental load models to reuse as much as possible
15
- # TODO: use pydantic models instead of dataclasses?
16
- # TODO: make the validation logic more readable (as in PermissionIncrementalLoad)
17
-
18
16
  TargetsPermissionDict: TypeAlias = dict[str, dict[str, bool]]
17
+ ConstructorType = TypeVar("ConstructorType", bound="ConstructorMixin")
19
18
 
20
19
 
21
- class PermissionType(Enum):
20
+ class PermissionType(str, Enum):
21
+ # NOTE: Start using StrEnum with Python 3.11
22
22
  user = "user"
23
23
  user_group = "userGroup"
24
24
 
25
25
 
26
- @dataclass(frozen=True)
27
- class PermissionIncrementalLoad:
28
- permission: str
29
- workspace_id: str
30
- id: str
31
- type: PermissionType
32
- is_active: bool
26
+ class ConstructorMixin:
27
+ @staticmethod
28
+ def _get_id_and_type(
29
+ permission: dict[str, Any],
30
+ ) -> tuple[str, PermissionType]:
31
+ user_id: str | None = permission.get("user_id")
32
+ user_group_id: str | None = permission.get("ug_id")
33
+ if user_id and user_group_id:
34
+ raise ValueError("Only one of user_id or ug_id must be present")
35
+ elif user_id:
36
+ return user_id, PermissionType.user
37
+ elif user_group_id:
38
+ return user_group_id, PermissionType.user_group
39
+ else:
40
+ raise ValueError("Either user_id or ug_id must be present")
33
41
 
34
42
  @classmethod
35
43
  def from_list_of_dicts(
36
- cls, data: list[dict[str, Any]]
37
- ) -> list["PermissionIncrementalLoad"]:
38
- """Creates a list of User objects from list of dicts."""
39
- id: str
44
+ cls: type[ConstructorType], data: list[dict[str, Any]]
45
+ ) -> list[ConstructorType]:
46
+ """Creates a list of instances from list of dicts."""
47
+ # NOTE: We can use typing.Self for the return type in Python 3.11
40
48
  permissions = []
41
49
  for permission in data:
42
- user_id: str | None = permission.get("user_id")
43
- user_group_id: str | None = permission.get("ug_id")
44
-
45
- if user_id is not None:
46
- target_type = PermissionType.user
47
- id = user_id
48
- elif user_group_id is not None:
49
- target_type = PermissionType.user_group
50
- id = user_group_id
51
-
52
- permissions.append(
53
- PermissionIncrementalLoad(
54
- permission=permission["ws_permissions"],
55
- workspace_id=permission["ws_id"],
56
- id=id,
57
- type=target_type,
58
- is_active=str(permission["is_active"]).lower() == "true",
59
- )
60
- )
50
+ permissions.append(cls.from_dict(permission))
61
51
  return permissions
62
52
 
53
+ @classmethod
54
+ @abstractmethod
55
+ def from_dict(cls, data: dict[str, Any]) -> Any:
56
+ """Construction form a dictionary to be implemented by subclasses."""
57
+ pass
58
+
63
59
 
64
- @dataclass(frozen=True)
65
- class PermissionFullLoad:
60
+ class PermissionIncrementalLoad(BaseModel, ConstructorMixin):
66
61
  permission: str
67
62
  workspace_id: str
68
- id: str
69
- type: PermissionType
63
+ id_: str
64
+ type_: PermissionType
65
+ is_active: bool
70
66
 
71
67
  @classmethod
72
- def from_list_of_dicts(
73
- cls, data: list[dict[str, Any]]
74
- ) -> list["PermissionFullLoad"]:
75
- """Creates a list of User objects from list of dicts."""
76
- permissions = []
77
- for permission in data:
78
- id = (
79
- permission["user_id"]
80
- if permission["user_id"]
81
- else permission["ug_id"]
82
- )
68
+ def from_dict(cls, data: dict[str, Any]) -> "PermissionIncrementalLoad":
69
+ """Returns an instance of PermissionIncrementalLoad from a dictionary."""
70
+ id_, target_type = cls._get_id_and_type(data)
71
+ return cls(
72
+ permission=data["ws_permissions"],
73
+ workspace_id=data["ws_id"],
74
+ id_=id_,
75
+ type_=target_type,
76
+ is_active=data["is_active"],
77
+ )
83
78
 
84
- if permission["user_id"]:
85
- target_type = PermissionType.user
86
- else:
87
- target_type = PermissionType.user_group
88
-
89
- permissions.append(
90
- PermissionFullLoad(
91
- permission=permission["ws_permissions"],
92
- workspace_id=permission["ws_id"],
93
- id=id,
94
- type=target_type,
95
- )
96
- )
97
- return permissions
98
79
 
80
+ class PermissionFullLoad(BaseModel, ConstructorMixin):
81
+ permission: str
82
+ workspace_id: str
83
+ id_: str
84
+ type_: PermissionType
85
+
86
+ @classmethod
87
+ def from_dict(cls, data: dict[str, Any]) -> "PermissionFullLoad":
88
+ """Returns an instance of PermissionFullLoad from a dictionary."""
89
+ id_, target_type = cls._get_id_and_type(data)
90
+ return cls(
91
+ permission=data["ws_permissions"],
92
+ workspace_id=data["ws_id"],
93
+ id_=id_,
94
+ type_=target_type,
95
+ )
99
96
 
100
- @dataclass
97
+
98
+ @attrs.define
101
99
  class PermissionDeclaration:
102
100
  users: TargetsPermissionDict
103
101
  user_groups: TargetsPermissionDict
@@ -192,7 +190,9 @@ class PermissionDeclaration:
192
190
  permissions=permission_declarations
193
191
  )
194
192
 
195
- def add_permission(self, permission: PermissionIncrementalLoad) -> None:
193
+ def add_incremental_permission(
194
+ self, permission: PermissionIncrementalLoad
195
+ ) -> None:
196
196
  """
197
197
  Adds WSPermission object into respective field within the instance.
198
198
  Handles duplicate permissions and different combinations of input
@@ -200,15 +200,15 @@ class PermissionDeclaration:
200
200
  """
201
201
  target_dict = (
202
202
  self.users
203
- if permission.type == PermissionType.user
203
+ if permission.type_ == PermissionType.user
204
204
  else self.user_groups
205
205
  )
206
206
 
207
- if permission.id not in target_dict:
208
- target_dict[permission.id] = {}
207
+ if permission.id_ not in target_dict:
208
+ target_dict[permission.id_] = {}
209
209
 
210
210
  is_active = permission.is_active
211
- target_permissions = target_dict[permission.id]
211
+ target_permissions = target_dict[permission.id_]
212
212
  permission_value = permission.permission
213
213
 
214
214
  if permission_value not in target_permissions:
@@ -225,6 +225,27 @@ class PermissionDeclaration:
225
225
  )
226
226
  target_permissions[permission_value] = is_active
227
227
 
228
+ def add_full_load_permission(self, permission: PermissionFullLoad) -> None:
229
+ """
230
+ Adds WSPermission object into respective field within the instance.
231
+ Handles duplicate permissions and different combinations of input
232
+ and upstream is_active permission states.
233
+ """
234
+ target_dict = (
235
+ self.users
236
+ if permission.type_ == PermissionType.user
237
+ else self.user_groups
238
+ )
239
+
240
+ if permission.id_ not in target_dict:
241
+ target_dict[permission.id_] = {}
242
+
243
+ target_permissions = target_dict[permission.id_]
244
+ permission_value = permission.permission
245
+
246
+ if permission_value not in target_permissions:
247
+ target_permissions[permission_value] = True
248
+
228
249
  def upsert(self, other: "PermissionDeclaration") -> None:
229
250
  """
230
251
  Modifies the owner object by merging with the other.
@@ -2,6 +2,8 @@
2
2
 
3
3
  """Module for provisioning user permissions in GoodData workspaces."""
4
4
 
5
+ from typing import TypeVar
6
+
5
7
  from gooddata_pipelines.api.exceptions import GoodDataApiException
6
8
  from gooddata_pipelines.provisioning.entities.users.models.permissions import (
7
9
  PermissionDeclaration,
@@ -14,6 +16,11 @@ from gooddata_pipelines.provisioning.entities.users.models.permissions import (
14
16
  from gooddata_pipelines.provisioning.provisioning import Provisioning
15
17
  from gooddata_pipelines.provisioning.utils.exceptions import BaseUserException
16
18
 
19
+ # Type variable for permission models (PermissionIncrementalLoad or PermissionFullLoad)
20
+ PermissionModel = TypeVar(
21
+ "PermissionModel", PermissionIncrementalLoad, PermissionFullLoad
22
+ )
23
+
17
24
 
18
25
  class PermissionProvisioner(
19
26
  Provisioning[PermissionFullLoad, PermissionIncrementalLoad]
@@ -72,7 +79,7 @@ class PermissionProvisioner(
72
79
 
73
80
  @staticmethod
74
81
  def _construct_declarations(
75
- permissions: list[PermissionIncrementalLoad],
82
+ permissions: list[PermissionIncrementalLoad] | list[PermissionFullLoad],
76
83
  ) -> WSPermissionsDeclarations:
77
84
  """Constructs workspace permission declarations from the input permissions."""
78
85
  ws_dict: WSPermissionsDeclarations = {}
@@ -82,7 +89,12 @@ class PermissionProvisioner(
82
89
  if ws_id not in ws_dict:
83
90
  ws_dict[ws_id] = PermissionDeclaration({}, {})
84
91
 
85
- ws_dict[ws_id].add_permission(permission)
92
+ if isinstance(permission, PermissionIncrementalLoad):
93
+ ws_dict[ws_id].add_incremental_permission(permission)
94
+ elif isinstance(permission, PermissionFullLoad):
95
+ ws_dict[ws_id].add_full_load_permission(permission)
96
+ else:
97
+ raise ValueError(f"Invalid permission type: {type(permission)}")
86
98
  return ws_dict
87
99
 
88
100
  def _check_user_group_exists(self, ug_id: str) -> None:
@@ -90,14 +102,14 @@ class PermissionProvisioner(
90
102
  self._api._sdk.catalog_user.get_user_group(ug_id)
91
103
 
92
104
  def _validate_permission(
93
- self, permission: PermissionIncrementalLoad
105
+ self, permission: PermissionFullLoad | PermissionIncrementalLoad
94
106
  ) -> None:
95
107
  """Validates if the permission is correctly defined."""
96
- if permission.type == PermissionType.user:
97
- self._api.get_user(permission.id, error_message="User not found")
108
+ if permission.type_ == PermissionType.user:
109
+ self._api.get_user(permission.id_, error_message="User not found")
98
110
  else:
99
111
  self._api.get_user_group(
100
- permission.id, error_message="User group not found"
112
+ permission.id_, error_message="User group not found"
101
113
  )
102
114
 
103
115
  self._api.get_workspace(
@@ -105,10 +117,12 @@ class PermissionProvisioner(
105
117
  )
106
118
 
107
119
  def _filter_invalid_permissions(
108
- self, permissions: list[PermissionIncrementalLoad]
109
- ) -> list[PermissionIncrementalLoad]:
120
+ self,
121
+ permissions: list[PermissionModel],
122
+ ) -> list[PermissionModel]:
110
123
  """Filters out invalid permissions from the input list."""
111
- valid_permissions: list[PermissionIncrementalLoad] = []
124
+ valid_permissions: list[PermissionModel] = []
125
+
112
126
  for permission in permissions:
113
127
  try:
114
128
  self._validate_permission(permission)
@@ -121,13 +135,15 @@ class PermissionProvisioner(
121
135
  valid_permissions.append(permission)
122
136
  return valid_permissions
123
137
 
124
- def _manage_permissions(
125
- self, permissions: list[PermissionIncrementalLoad]
126
- ) -> None:
127
- """Manages permissions for a list of workspaces.
128
- Modify upstream workspace declarations for each input workspace and skip non-existent ws_ids
138
+ def _provision_incremental_load(self) -> None:
139
+ """Provisiones permissions for a list of workspaces.
140
+
141
+ Modifies existing upstream workspace permission declarations for each
142
+ input workspace and skips rest of the workspaces.
129
143
  """
130
- valid_permissions = self._filter_invalid_permissions(permissions)
144
+ valid_permissions = self._filter_invalid_permissions(
145
+ self.source_group_incremental
146
+ )
131
147
 
132
148
  input_declarations = self._construct_declarations(valid_permissions)
133
149
 
@@ -145,9 +161,21 @@ class PermissionProvisioner(
145
161
  self._api.put_declarative_permissions(ws_id, ws_permissions)
146
162
  self.logger.info(f"Updated permissions for workspace {ws_id}")
147
163
 
148
- def _provision_incremental_load(self) -> None:
149
- """Provision permissions based on the source group."""
150
- self._manage_permissions(self.source_group_incremental)
151
-
152
164
  def _provision_full_load(self) -> None:
153
- raise NotImplementedError("Not implemented yet.")
165
+ """Provisions permissions for selected of workspaces.
166
+
167
+ Modifies upstream workspace declarations for each input workspace and
168
+ skips non-existent workspace ids. Overwrites any existing configuration
169
+ of the workspace permissions.
170
+ """
171
+ valid_permissions = self._filter_invalid_permissions(
172
+ self.source_group_full
173
+ )
174
+
175
+ input_declarations = self._construct_declarations(valid_permissions)
176
+
177
+ for ws_id, declaration in input_declarations.items():
178
+ ws_permissions = declaration.to_sdk_api()
179
+
180
+ self._api.put_declarative_permissions(ws_id, ws_permissions)
181
+ self.logger.info(f"Updated permissions for workspace {ws_id}")
@@ -36,6 +36,7 @@ class Provisioning(Generic[TFullLoadSourceData, TIncrementalSourceData]):
36
36
  cls: Type[TProvisioning], host: str, token: str
37
37
  ) -> TProvisioning:
38
38
  """Creates a provisioner instance using provided host and token."""
39
+ cls._validate_credentials(host, token)
39
40
  return cls(host=host, token=token)
40
41
 
41
42
  @classmethod
@@ -48,6 +49,16 @@ class Provisioning(Generic[TFullLoadSourceData, TIncrementalSourceData]):
48
49
  content = profile_content(profile, profiles_path)
49
50
  return cls(**content)
50
51
 
52
+ @staticmethod
53
+ def _validate_credentials(host: str, token: str) -> None:
54
+ """Validates the credentials."""
55
+ if (not host) and (not token):
56
+ raise ValueError("Host and token are required.")
57
+ if not host:
58
+ raise ValueError("Host is required.")
59
+ if not token:
60
+ raise ValueError("Token is required.")
61
+
51
62
  @staticmethod
52
63
  def _create_groups(
53
64
  source_id: set[str], panther_id: set[str]
@@ -95,13 +106,9 @@ class Provisioning(Generic[TFullLoadSourceData, TIncrementalSourceData]):
95
106
 
96
107
  try:
97
108
  self._provision_full_load()
98
- self.logger.info("Provisioning completed successfully.")
109
+ self.logger.info("Provisioning completed.")
99
110
  except Exception as e:
100
- self.fatal_exception = str(e)
101
- self.logger.error(
102
- f"Provisioning failed. Error: {self.fatal_exception} "
103
- + f"Context: {e.__dict__}"
104
- )
111
+ self._handle_fatal_exception(e)
105
112
 
106
113
  def incremental_load(
107
114
  self, source_data: list[TIncrementalSourceData]
@@ -111,22 +118,34 @@ class Provisioning(Generic[TFullLoadSourceData, TIncrementalSourceData]):
111
118
  Incremental provisioning is used to modify a subset of the upstream workspaces
112
119
  based on the source data provided.
113
120
  """
121
+ # TODO: validate the data type of source group at runtime
114
122
  self.source_group_incremental = source_data
115
123
 
116
124
  try:
117
125
  self._provision_incremental_load()
118
- self.logger.info("Provisioning completed successfully.")
126
+ self.logger.info("Provisioning completed.")
119
127
  except Exception as e:
120
- self.fatal_exception = str(e)
121
- self.logger.error(
122
- f"Provisioning failed. Error: {self.fatal_exception} "
123
- + f"Context: {e.__dict__}"
124
- )
125
-
126
- # TODO: implement a sceond provisioning method and name the two differently:
127
- # 1) provision_incremental - will use the is_active logic, such as user provisioning now
128
- # 2) provision_full - full load of the source data, like workspaces now
129
- # Each will have its own implementation and source data model.
130
- # Both use cases are required and need to be supported.
131
- # This will also improve the clarity of the code as now provisioning of each
132
- # entity works differently, leading to confusion.
128
+ self._handle_fatal_exception(e)
129
+
130
+ def _handle_fatal_exception(self, e: Exception) -> None:
131
+ """Handles fatal exceptions during provisioning.
132
+
133
+ Logs the exception content. Re-raises the exception if there is no
134
+ subscriber to the logger.
135
+ """
136
+ self.fatal_exception = str(e)
137
+
138
+ if hasattr(e, "__dict__"):
139
+ exception_context = f"Context: {e.__dict__}"
140
+ else:
141
+ exception_context = ""
142
+
143
+ exception_message = (
144
+ f"Provisioning failed. Error: {self.fatal_exception}. "
145
+ + exception_context
146
+ )
147
+
148
+ self.logger.error(exception_message)
149
+
150
+ if not self.logger.subscribers:
151
+ raise Exception(exception_message)
@@ -1,23 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gooddata-pipelines
3
- Version: 1.48.1.dev4
3
+ Version: 1.49.1.dev1
4
+ Summary: GoodData Cloud lifecycle automation pipelines
4
5
  Author-email: GoodData <support@gooddata.com>
5
6
  License: MIT
6
7
  License-File: LICENSE.txt
7
8
  Requires-Python: >=3.10
8
9
  Requires-Dist: boto3-stubs<2.0.0,>=1.39.3
9
10
  Requires-Dist: boto3<2.0.0,>=1.39.3
10
- Requires-Dist: gooddata-sdk~=1.48.1.dev4
11
+ Requires-Dist: gooddata-sdk~=1.49.1.dev1
11
12
  Requires-Dist: pydantic<3.0.0,>=2.11.3
12
13
  Requires-Dist: requests<3.0.0,>=2.32.3
13
14
  Requires-Dist: types-pyyaml<7.0.0,>=6.0.12.20250326
14
15
  Requires-Dist: types-requests<3.0.0,>=2.32.0
15
- Provides-Extra: dev
16
- Requires-Dist: moto<6.0.0,>=5.1.6; extra == 'dev'
17
- Requires-Dist: mypy<2.0.0,>=1.16.0; extra == 'dev'
18
- Requires-Dist: pytest-mock<4.0.0,>=3.14.0; extra == 'dev'
19
- Requires-Dist: pytest<9.0.0,>=8.3.5; extra == 'dev'
20
- Requires-Dist: ruff<0.12.0,>=0.11.2; extra == 'dev'
21
16
  Description-Content-Type: text/markdown
22
17
 
23
18
  # GoodData Pipelines
@@ -23,7 +23,7 @@ gooddata_pipelines/backup_and_restore/storage/s3_storage.py,sha256=iRtMtDq_C1b_J
23
23
  gooddata_pipelines/logger/__init__.py,sha256=W-fJvMStnsDUY52AYFhx_LnS2cSCFNf3bB47Iew2j04,129
24
24
  gooddata_pipelines/logger/logger.py,sha256=yIMdvqsmOSGQLI4U_tQwxX5E2q_FXUu0Ko7Hv39slFM,3549
25
25
  gooddata_pipelines/provisioning/__init__.py,sha256=RZDEiv8nla4Jwa2TZXUdp1NSxg2_-lLqz4h7k2c4v5Y,854
26
- gooddata_pipelines/provisioning/provisioning.py,sha256=ZVD-Jz_MyLDy7f1D62oJ58sHfW03_LNO7Bguuv4C4xA,5042
26
+ gooddata_pipelines/provisioning/provisioning.py,sha256=RSxp3bZgdJx3WfbkwQrV1W7dRlyngqfppCtCfVG7BbA,5357
27
27
  gooddata_pipelines/provisioning/assets/wdf_setting.json,sha256=nxOLGZkEQiMdARcUDER5ygqr3Zu-MQlLlUyXVhPUq64,280
28
28
  gooddata_pipelines/provisioning/entities/__init__.py,sha256=-BG28PGDbalLyZGQjpFG0pjdIvtf25ut0r8ZwZVbi4s,32
29
29
  gooddata_pipelines/provisioning/entities/user_data_filters/__init__.py,sha256=-BG28PGDbalLyZGQjpFG0pjdIvtf25ut0r8ZwZVbi4s,32
@@ -31,11 +31,11 @@ gooddata_pipelines/provisioning/entities/user_data_filters/user_data_filters.py,
31
31
  gooddata_pipelines/provisioning/entities/user_data_filters/models/__init__.py,sha256=-BG28PGDbalLyZGQjpFG0pjdIvtf25ut0r8ZwZVbi4s,32
32
32
  gooddata_pipelines/provisioning/entities/user_data_filters/models/udf_models.py,sha256=y0q5E91AhxIkf_EHW0swCjNUkiiAOFXarAhvjUKVVKw,740
33
33
  gooddata_pipelines/provisioning/entities/users/__init__.py,sha256=-BG28PGDbalLyZGQjpFG0pjdIvtf25ut0r8ZwZVbi4s,32
34
- gooddata_pipelines/provisioning/entities/users/permissions.py,sha256=05RptbWJ5L9eZ12R7orG3-wF-w69IwlRzHitMzGokiY,5781
34
+ gooddata_pipelines/provisioning/entities/users/permissions.py,sha256=dZUxzTscIpWPRvANUVocjAfpUSvJ7ImBiABBTIeguyE,6850
35
35
  gooddata_pipelines/provisioning/entities/users/user_groups.py,sha256=Up36pwwlOFS_IdYetViZ7gUHfV2hIgXL4th_k9D31Eo,8266
36
36
  gooddata_pipelines/provisioning/entities/users/users.py,sha256=1B1bMk8ysughCoCJs1aX0bI9iUIeAc1hIUyJ0hWyC5M,6503
37
37
  gooddata_pipelines/provisioning/entities/users/models/__init__.py,sha256=-BG28PGDbalLyZGQjpFG0pjdIvtf25ut0r8ZwZVbi4s,32
38
- gooddata_pipelines/provisioning/entities/users/models/permissions.py,sha256=Hx-8ac8n5xOyysJJCXu2XpOXL1IqH_w9LKck2qseWBs,8377
38
+ gooddata_pipelines/provisioning/entities/users/models/permissions.py,sha256=JCyItTqDdyDqB72O5f32IOh1sAiVK-DwFyXyllNU-v4,9279
39
39
  gooddata_pipelines/provisioning/entities/users/models/user_groups.py,sha256=TjlP6oABK6UP7nMKNMlLk3M62eNf9e-3LdKI9-VFwi8,2007
40
40
  gooddata_pipelines/provisioning/entities/users/models/users.py,sha256=rKtiRxtelLphw-_BbD-AM_-hPrpp0xqEr1jmuU_oJVg,3767
41
41
  gooddata_pipelines/provisioning/entities/workspaces/__init__.py,sha256=-BG28PGDbalLyZGQjpFG0pjdIvtf25ut0r8ZwZVbi4s,32
@@ -48,7 +48,7 @@ gooddata_pipelines/provisioning/utils/__init__.py,sha256=-BG28PGDbalLyZGQjpFG0pj
48
48
  gooddata_pipelines/provisioning/utils/context_objects.py,sha256=sM22hMsxE0XLI1TU0Vs-2kK0vf4YrB1musoAg__4bjc,936
49
49
  gooddata_pipelines/provisioning/utils/exceptions.py,sha256=1WnAOlPhqOf0xRcvn70lxAlLb8Oo6m6WCYS4hj9uzDU,3630
50
50
  gooddata_pipelines/provisioning/utils/utils.py,sha256=_Tk-mFgbIGpCixDCF9e-3ZYd-g5Jb3SJiLSH465k4jY,2846
51
- gooddata_pipelines-1.48.1.dev4.dist-info/METADATA,sha256=Tjzry0iKQrR-LkYMX0PRWeBzXBT2XD9_8FRTMOWOMp4,3750
52
- gooddata_pipelines-1.48.1.dev4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
- gooddata_pipelines-1.48.1.dev4.dist-info/licenses/LICENSE.txt,sha256=PNC7WXGIo6OKkNoPLRxlVrw6jaLcjSTUsSxy9Xcu9Jo,560365
54
- gooddata_pipelines-1.48.1.dev4.dist-info/RECORD,,
51
+ gooddata_pipelines-1.49.1.dev1.dist-info/METADATA,sha256=_FSW5vT-giekFNSkPNpNZ5X5YEaPHOJql7kejY-50ew,3522
52
+ gooddata_pipelines-1.49.1.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
+ gooddata_pipelines-1.49.1.dev1.dist-info/licenses/LICENSE.txt,sha256=PNC7WXGIo6OKkNoPLRxlVrw6jaLcjSTUsSxy9Xcu9Jo,560365
54
+ gooddata_pipelines-1.49.1.dev1.dist-info/RECORD,,