Skip to content

Commit 6af7911

Browse files
committed
chore(gooddata-pipelines): replace dataclasses with attrs
1 parent 9a473f9 commit 6af7911

File tree

8 files changed

+40
-41
lines changed

8 files changed

+40
-41
lines changed

gooddata-pipelines/TODO.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,10 @@ A list of outstanding tasks, features, or technical debt to be addressed in this
1010

1111
- [ ] Integrate with GoodDataApiClient
1212
- [ ] Consider replacing the SdkMethods wrapper with direct calls to the SDK methods
13-
- [ ] Consider using orjson library instead of json to load test data
1413
- [ ] Cleanup custom exceptions
1514
- [ ] Improve test coverage. Write missing unit tests for legacy code (e.g., user data filters)
1615

1716
## Documentation
1817

1918
- [ ] Improve package README
20-
- [ ] Workspace provisioning
21-
- [ ] User provisioning
22-
- [ ] User group provisioning
23-
- [ ] Permission provisioning
2419
- [ ] User data filter provisioning

gooddata-pipelines/gooddata_pipelines/backup_and_restore/backup_input_processor.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# (C) 2025 GoodData Corporation
22

3-
from dataclasses import dataclass
4-
3+
import attrs
54
import requests
65

76
from gooddata_pipelines.api import GoodDataApi
@@ -46,7 +45,7 @@ def set_endpoints(self) -> None:
4645
)
4746
self.all_workspaces_endpoint = f"{self.base_workspace_endpoint}?page=0&size={self.page_size}&sort=name,asc&metaInclude=page"
4847

49-
@dataclass
48+
@attrs.define
5049
class _ProcessDataOutput:
5150
workspace_ids: list[str]
5251
sub_parents: list[str] | None = None

gooddata-pipelines/gooddata_pipelines/backup_and_restore/backup_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
import tempfile
77
import time
88
import traceback
9-
from dataclasses import dataclass
109
from pathlib import Path
1110
from typing import Any, Type
1211

12+
import attrs
1313
import requests
1414
import yaml
1515
from gooddata_sdk.utils import PROFILES_FILE_PATH, profile_content
@@ -40,7 +40,7 @@
4040
from gooddata_pipelines.utils.rate_limiter import RateLimiter
4141

4242

43-
@dataclass
43+
@attrs.define
4444
class BackupBatch:
4545
list_of_ids: list[str]
4646

gooddata-pipelines/gooddata_pipelines/backup_and_restore/constants.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# (C) 2025 GoodData Corporation
22
import datetime
3-
from dataclasses import dataclass
43

4+
import attrs
55
from gooddata_sdk._version import __version__ as sdk_version
66

77

8-
@dataclass(frozen=True)
8+
@attrs.frozen
99
class DirNames:
1010
"""
1111
Folder names used in the SDK backup process:
@@ -21,14 +21,14 @@ class DirNames:
2121
UDF = "user_data_filters"
2222

2323

24-
@dataclass(frozen=True)
24+
@attrs.frozen
2525
class ApiDefaults:
2626
DEFAULT_PAGE_SIZE = 100
2727
DEFAULT_BATCH_SIZE = 100
2828
DEFAULT_API_CALLS_PER_SECOND = 1.0
2929

3030

31-
@dataclass(frozen=True)
31+
@attrs.frozen
3232
class BackupSettings(ApiDefaults):
3333
MAX_RETRIES = 3
3434
RETRY_DELAY = 5 # seconds

gooddata-pipelines/gooddata_pipelines/provisioning/entities/user_data_filters/models/udf_models.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,20 @@
22

33
"""This module defines data models for user data filters in a GoodData workspace."""
44

5-
# TODO: consider using attrs instead of dataclasses for these models. Dataclasses
6-
# have different functionality per Python version (not package version).
7-
8-
from dataclasses import dataclass, field
9-
5+
import attrs
106
from pydantic import BaseModel, ConfigDict
117

128

13-
@dataclass
9+
@attrs.define
1410
class UserDataFilterGroup:
1511
udf_id: str
1612
udf_values: list[str]
1713

1814

19-
@dataclass
15+
@attrs.define
2016
class WorkspaceUserDataFilters:
2117
workspace_id: str
22-
user_data_filters: list["UserDataFilterGroup"] = field(default_factory=list)
18+
user_data_filters: list["UserDataFilterGroup"] = attrs.field(factory=list)
2319

2420

2521
class UserDataFilterFullLoad(BaseModel):

gooddata-pipelines/gooddata_pipelines/provisioning/entities/user_data_filters/user_data_filters.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ class UserDataFilterProvisioner(
5050
ldm_column_name: str = ""
5151
maql_column_name: str = ""
5252

53+
FULL_LOAD_TYPE = UserDataFilterFullLoad
54+
5355
def set_ldm_column_name(self, ldm_column_name: str) -> None:
5456
"""Set the LDM column name for user data filters.
5557
@@ -214,8 +216,6 @@ def _provision_full_load(self) -> None:
214216
)
215217
self._create_user_data_filters(grouped_db_user_data_filters)
216218

217-
self.logger.info("User data filters provisioning completed")
218-
219219
def _provision_incremental_load(self) -> None:
220220
"""Provision user data filters in GoodData workspaces."""
221221
raise NotImplementedError("Not implemented yet.")

gooddata-pipelines/gooddata_pipelines/provisioning/utils/utils.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
"""Module for utilities used in GoodData Pipelines provisioning."""
44

5+
from typing import Any, cast
6+
57
import attrs
68
from requests import Response
79

@@ -11,9 +13,8 @@ class AttributesMixin:
1113
Mixin class to provide a method for getting attributes of an object which may or may not exist.
1214
"""
1315

14-
@staticmethod
1516
def get_attrs(
16-
*objects: object, overrides: dict[str, str] | None = None
17+
self, *objects: object, overrides: dict[str, str] | None = None
1718
) -> dict[str, str]:
1819
"""
1920
Returns a dictionary of attributes from the given objects.
@@ -27,11 +28,11 @@ def get_attrs(
2728
"""
2829
# TODO: This might not work great with nested objects, values which are lists of objects etc.
2930
# If we care about parsing the logs back from the string, we should consider some other approach
30-
attrs: dict[str, str] = {}
31+
attributes: dict[str, str] = {}
3132
for context_object in objects:
3233
if isinstance(context_object, Response):
3334
# for request.Response objects, keys need to be renamed to match the log schema
34-
attrs.update(
35+
attributes.update(
3536
{
3637
"http_status": str(context_object.status_code),
3738
"http_method": getattr(
@@ -42,23 +43,31 @@ def get_attrs(
4243
),
4344
}
4445
)
46+
elif attrs.has(type(context_object)):
47+
for key, value in attrs.asdict(
48+
cast(attrs.AttrsInstance, context_object)
49+
).items():
50+
self._add_to_dict(attributes, key, value)
4551
else:
4652
# Generic handling for other objects
4753
for key, value in context_object.__dict__.items():
48-
if value is None:
49-
continue
50-
51-
if isinstance(value, list):
52-
attrs[key] = ", ".join(
53-
str(list_item) for list_item in value
54-
)
55-
else:
56-
attrs[key] = str(value)
54+
self._add_to_dict(attributes, key, value)
5755

5856
if overrides:
59-
attrs.update(overrides)
57+
attributes.update(overrides)
58+
59+
return attributes
60+
61+
def _add_to_dict(
62+
self, attributes: dict[str, str], key: str, value: Any
63+
) -> None:
64+
if value is None:
65+
return
6066

61-
return attrs
67+
if isinstance(value, list):
68+
attributes[key] = ", ".join(str(list_item) for list_item in value)
69+
else:
70+
attributes[key] = str(value)
6271

6372

6473
@attrs.define

gooddata-pipelines/tests/provisioning/entities/users/test_users.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# (C) 2025 GoodData Corporation
2-
from dataclasses import dataclass
32
from typing import Literal, Optional
43

4+
import attrs
55
import orjson
66
import pytest
77
from gooddata_api_client.exceptions import NotFoundException # type: ignore
@@ -29,7 +29,7 @@
2929
TEST_DATA_SUBDIR = f"{TEST_DATA_DIR}/provisioning/entities/users"
3030

3131

32-
@dataclass
32+
@attrs.define
3333
class MockUser:
3434
id: str
3535
firstname: Optional[str]

0 commit comments

Comments
 (0)