Skip to content

Commit 94221e3

Browse files
committed
Sonar analysis
1 parent 3ddb6a3 commit 94221e3

8 files changed

Lines changed: 244 additions & 16 deletions

File tree

py_src/fusion/attributes.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from dataclasses import dataclass, field, fields
5+
from dataclasses import MISSING, dataclass, field, fields
66
from typing import TYPE_CHECKING, Any, cast
77

88
import numpy as np
@@ -260,7 +260,40 @@ def _from_dict(cls: type[Attribute], data: dict[str, Any]) -> Attribute:
260260
data = {k: (None if pd.isna(v) else v) for k, v in data.items() if k in keys}
261261
if "data_type" in data:
262262
data["data_type"] = Types[data["data_type"].strip().rsplit(".", maxsplit=1)[-1].title()]
263-
return cls(**data)
263+
field_map = {field_.name: field_ for field_ in fields(cls)}
264+
265+
def _value(name: str) -> Any:
266+
if name in data:
267+
return data[name]
268+
field_ = field_map[name]
269+
if field_.default_factory is not MISSING:
270+
return field_.default_factory()
271+
return field_.default
272+
273+
return cls(
274+
identifier=data["identifier"],
275+
index=data["index"],
276+
data_type=_value("data_type"),
277+
title=_value("title"),
278+
description=_value("description"),
279+
is_dataset_key=_value("is_dataset_key"),
280+
source=_value("source"),
281+
source_field_id=_value("source_field_id"),
282+
is_internal_dataset_key=_value("is_internal_dataset_key"),
283+
is_externally_visible=_value("is_externally_visible"),
284+
unit=_value("unit"),
285+
multiplier=_value("multiplier"),
286+
is_propagation_eligible=_value("is_propagation_eligible"),
287+
is_metric=_value("is_metric"),
288+
available_from=_value("available_from"),
289+
deprecated_from=_value("deprecated_from"),
290+
term=_value("term"),
291+
dataset=_value("dataset"),
292+
attribute_type=_value("attribute_type"),
293+
application_id=_value("application_id"),
294+
publisher=_value("publisher"),
295+
is_key_data_element=_value("is_key_data_element"),
296+
)
264297

265298
def from_object(
266299
self,

py_src/fusion/dataset.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import json as js
6-
from dataclasses import dataclass, field, fields
6+
from dataclasses import MISSING, dataclass, field, fields
77
from typing import TYPE_CHECKING, Any
88

99
import pandas as pd
@@ -314,7 +314,55 @@ def _from_dict(cls: type[Dataset], data: dict[str, Any]) -> Dataset:
314314
data = {k: v for k, v in data.items() if k in keys}
315315
if "type" in data:
316316
data["type_"] = data.pop("type")
317-
return cls(**data)
317+
field_map = {field_.name: field_ for field_ in fields(cls)}
318+
319+
def _value(name: str) -> Any:
320+
if name in data:
321+
return data[name]
322+
field_ = field_map[name]
323+
if field_.default_factory is not MISSING:
324+
return field_.default_factory()
325+
return field_.default
326+
327+
return cls(
328+
identifier=data["identifier"],
329+
title=_value("title"),
330+
category=_value("category"),
331+
description=_value("description"),
332+
frequency=_value("frequency"),
333+
is_internal_only_dataset=_value("is_internal_only_dataset"),
334+
is_third_party_data=_value("is_third_party_data"),
335+
is_restricted=_value("is_restricted"),
336+
is_raw_data=_value("is_raw_data"),
337+
maintainer=_value("maintainer"),
338+
source=_value("source"),
339+
region=_value("region"),
340+
publisher=_value("publisher"),
341+
product=_value("product"),
342+
sub_category=_value("sub_category"),
343+
tags=_value("tags"),
344+
created_date=_value("created_date"),
345+
modified_date=_value("modified_date"),
346+
delivery_channel=_value("delivery_channel"),
347+
language=_value("language"),
348+
status=_value("status"),
349+
type_=_value("type_"),
350+
container_type=_value("container_type"),
351+
snowflake=_value("snowflake"),
352+
complexity=_value("complexity"),
353+
is_immutable=_value("is_immutable"),
354+
is_mnpi=_value("is_mnpi"),
355+
is_pci=_value("is_pci"),
356+
is_pii=_value("is_pii"),
357+
is_client=_value("is_client"),
358+
is_public=_value("is_public"),
359+
is_internal=_value("is_internal"),
360+
is_confidential=_value("is_confidential"),
361+
is_highly_confidential=_value("is_highly_confidential"),
362+
is_active=_value("is_active"),
363+
owners=_value("owners"),
364+
application_id=_value("application_id"),
365+
)
318366

319367
@classmethod
320368
def _from_csv(cls: type[Dataset], file_path: str, identifier: str | None = None) -> Dataset:

py_src/fusion/fusion_filesystem.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,19 +1002,23 @@ def _compute_checksum_from_data(self, data: bytes, algorithm: str, is_multipart:
10021002

10031003
elif algorithm == "SHA-1":
10041004
if is_multipart:
1005-
inner_hash = hashlib.sha1(data).digest()
1006-
outer_hash = hashlib.sha1(inner_hash).digest()
1005+
inner_hash = hashlib.sha1(data).digest() # NOSONAR(S4790) legacy API checksum
1006+
outer_hash = hashlib.sha1(inner_hash).digest() # NOSONAR(S4790) legacy API checksum
10071007
return base64.b64encode(outer_hash).decode("ascii")
10081008
else:
1009-
return base64.b64encode(hashlib.sha1(data).digest()).decode("ascii")
1009+
return base64.b64encode(hashlib.sha1(data).digest()).decode(
1010+
"ascii"
1011+
) # NOSONAR(S4790) legacy API checksum
10101012

10111013
elif algorithm == "MD5":
10121014
if is_multipart:
1013-
inner_hash = hashlib.md5(data).digest()
1014-
outer_hash = hashlib.md5(inner_hash).digest()
1015+
inner_hash = hashlib.md5(data).digest() # NOSONAR(S4790) legacy API checksum
1016+
outer_hash = hashlib.md5(inner_hash).digest() # NOSONAR(S4790) legacy API checksum
10151017
return base64.b64encode(outer_hash).decode("ascii")
10161018
else:
1017-
return base64.b64encode(hashlib.md5(data).digest()).decode("ascii")
1019+
return base64.b64encode(hashlib.md5(data).digest()).decode( # NOSONAR(S4790) legacy API checksum
1020+
"ascii"
1021+
)
10181022

10191023
elif algorithm == "CRC64NVME":
10201024
crc_value = aws_checksums.crc64nvme(data)
@@ -1243,7 +1247,8 @@ async def put_data() -> AsyncGenerator[dict[Any, Any], None]:
12431247
operation_id = await resp.json()
12441248

12451249
operation_id = operation_id["operationId"]
1246-
resps = [resp async for resp in put_data()]
1250+
put_data_iterable: AsyncGenerator[dict[Any, Any], None] = put_data()
1251+
resps = [resp async for resp in put_data_iterable]
12471252
kw = self.kwargs.copy()
12481253
kw.update({"headers": headers})
12491254
kw = FusionHTTPFileSystem._update_kwargs(kw, headers, additional_headers)

py_src/fusion/product.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import json as js
6-
from dataclasses import dataclass, field, fields
6+
from dataclasses import MISSING, dataclass, field, fields
77
from typing import TYPE_CHECKING, Any
88

99
import pandas as pd
@@ -221,7 +221,38 @@ def _from_dict(cls: type[Product], data: dict[str, Any]) -> Product:
221221
keys = [f.name for f in fields(cls)]
222222
data = {camel_to_snake(k): v for k, v in data.items()}
223223
data = {k: v for k, v in data.items() if k in keys}
224-
return cls(**data)
224+
field_map = {field_.name: field_ for field_ in fields(cls)}
225+
226+
def _value(name: str) -> Any:
227+
if name in data:
228+
return data[name]
229+
field_ = field_map[name]
230+
if field_.default_factory is not MISSING:
231+
return field_.default_factory()
232+
return field_.default
233+
234+
return cls(
235+
identifier=data["identifier"],
236+
title=_value("title"),
237+
category=_value("category"),
238+
short_abstract=_value("short_abstract"),
239+
description=_value("description"),
240+
is_active=_value("is_active"),
241+
is_restricted=_value("is_restricted"),
242+
maintainer=_value("maintainer"),
243+
region=_value("region"),
244+
publisher=_value("publisher"),
245+
sub_category=_value("sub_category"),
246+
tag=_value("tag"),
247+
delivery_channel=_value("delivery_channel"),
248+
theme=_value("theme"),
249+
release_date=_value("release_date"),
250+
language=_value("language"),
251+
status=_value("status"),
252+
image=_value("image"),
253+
logo=_value("logo"),
254+
dataset=_value("dataset"),
255+
)
225256

226257
@classmethod
227258
def _from_csv(cls: type[Product], file_path: str, identifier: str | None = None) -> Product:

py_tests/test_attributes.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,33 @@ def test_attribute_class_from_dict() -> None:
135135
assert test_attribute.attributeType is None
136136

137137

138+
def test_attribute_class_from_dict_with_extended_fields() -> None:
139+
"""Test attribute class from dict with optional fields."""
140+
test_attribute = Attribute._from_dict(
141+
{
142+
"title": "Test Attribute",
143+
"identifier": "Test Attribute",
144+
"index": 0,
145+
"dataType": "string",
146+
"sourceFieldId": "Original Field",
147+
"isInternalDatasetKey": True,
148+
"isExternallyVisible": False,
149+
"deprecatedFrom": "May 5, 2021",
150+
"applicationId": {"id": "123", "type": "Application"},
151+
"publisher": "JPM",
152+
"isKeyDataElement": True,
153+
}
154+
)
155+
156+
assert test_attribute.sourceFieldId == "Original_Field"
157+
assert test_attribute.isInternalDatasetKey is True
158+
assert test_attribute.isExternallyVisible is False
159+
assert test_attribute.deprecatedFrom == "2021-05-05"
160+
assert test_attribute.applicationId == {"id": "123", "type": "Application"}
161+
assert test_attribute.publisher == "JPM"
162+
assert test_attribute.isKeyDataElement is True
163+
164+
138165
def test_attribute_class_from_object_dict() -> None:
139166
"""Test attribute class from object"""
140167
test_dict = {

py_tests/test_dataset.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,42 @@ def test_dataset_class_from_dict() -> None:
268268
assert test_dataset.is_active is None
269269

270270

271+
def test_dataset_class_from_dict_with_aliases() -> None:
272+
"""Test Dataset._from_dict handles aliased camelCase keys."""
273+
test_dataset = Dataset._from_dict(
274+
{
275+
"title": "Test Dataset",
276+
"identifier": "Test Dataset",
277+
"tag": ["tag1", "tag2"],
278+
"type": "Source",
279+
"applicationId": {"id": "123", "type": "Application"},
280+
"owners": ["owner1", "owner2"],
281+
}
282+
)
283+
284+
assert test_dataset.tags == ["tag1", "tag2"]
285+
assert test_dataset.type_ == "Source"
286+
assert test_dataset.application_id == {"id": "123", "type": "Application"}
287+
assert test_dataset.owners == ["owner1", "owner2"]
288+
289+
290+
def test_dataset_class_from_dict_uses_fresh_default_factory_list() -> None:
291+
"""Test Dataset._from_dict uses a fresh delivery_channel default list."""
292+
first_dataset = Dataset._from_dict({"identifier": "first_dataset"})
293+
second_dataset = Dataset._from_dict({"identifier": "second_dataset"})
294+
295+
assert isinstance(first_dataset.delivery_channel, list)
296+
assert isinstance(second_dataset.delivery_channel, list)
297+
assert first_dataset.delivery_channel == ["API"]
298+
assert second_dataset.delivery_channel == ["API"]
299+
assert first_dataset.delivery_channel is not second_dataset.delivery_channel
300+
301+
first_dataset.delivery_channel.append("SFTP")
302+
303+
assert first_dataset.delivery_channel == ["API", "SFTP"]
304+
assert second_dataset.delivery_channel == ["API"]
305+
306+
271307
def test_dataset_class_from_csv(mock_dataset_pd_read_csv: Generator[pd.DataFrame, Any, None]) -> None: # noqa: ARG001
272308
"""Test Dataset class."""
273309
test_dataset = Dataset._from_csv("datasets.csv")

py_tests/test_product.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,54 @@ def test_product_class_from_dict() -> None:
110110
assert test_product.dataset is None
111111

112112

113+
def test_product_class_from_dict_with_optional_fields() -> None:
114+
"""Test Product._from_dict maps optional fields directly."""
115+
test_product = Product._from_dict(
116+
{
117+
"title": "Test Product",
118+
"identifier": "TEST_PRODUCT",
119+
"shortAbstract": "Short Abstract",
120+
"isRestricted": False,
121+
"maintainer": ["Maintainer"],
122+
"subCategory": ["Sub Category"],
123+
"tag": ["Tag"],
124+
"dataset": ["DATASET_1"],
125+
}
126+
)
127+
128+
assert test_product.shortAbstract == "Short Abstract"
129+
assert test_product.isRestricted is False
130+
assert test_product.maintainer == ["Maintainer"]
131+
assert test_product.subCategory == ["Sub Category"]
132+
assert test_product.tag == ["Tag"]
133+
assert test_product.dataset == ["DATASET_1"]
134+
135+
136+
def test_product_class_from_dict_uses_fresh_default_factory_lists() -> None:
137+
"""Test Product._from_dict uses fresh list defaults for default_factory fields."""
138+
first_product = Product._from_dict({"identifier": "FIRST_PRODUCT"})
139+
second_product = Product._from_dict({"identifier": "SECOND_PRODUCT"})
140+
141+
assert isinstance(first_product.region, list)
142+
assert isinstance(second_product.region, list)
143+
assert isinstance(first_product.delivery_channel, list)
144+
assert isinstance(second_product.delivery_channel, list)
145+
assert first_product.region == ["Global"]
146+
assert second_product.region == ["Global"]
147+
assert first_product.region is not second_product.region
148+
assert first_product.delivery_channel == ["API"]
149+
assert second_product.delivery_channel == ["API"]
150+
assert first_product.delivery_channel is not second_product.delivery_channel
151+
152+
first_product.region.append("EMEA")
153+
first_product.delivery_channel.append("SFTP")
154+
155+
assert first_product.region == ["Global", "EMEA"]
156+
assert second_product.region == ["Global"]
157+
assert first_product.delivery_channel == ["API", "SFTP"]
158+
assert second_product.delivery_channel == ["API"]
159+
160+
113161
def test_product_class_from_csv(mock_product_pd_read_csv: Generator[pd.DataFrame, Any, None]) -> None: # noqa: ARG001
114162
"""Test the Product class."""
115163
test_product = Product._from_csv("products.csv")

uv.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)