File: dict.py

package info (click to toggle)
python-pyfunceble 4.3.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,144 kB
  • sloc: python: 27,918; sh: 142; makefile: 48
file content (436 lines) | stat: -rw-r--r-- 13,774 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
"""
The tool to check the availability or syntax of domain, IP or URL.

::


    ██████╗ ██╗   ██╗███████╗██╗   ██╗███╗   ██╗ ██████╗███████╗██████╗ ██╗     ███████╗
    ██╔══██╗╚██╗ ██╔╝██╔════╝██║   ██║████╗  ██║██╔════╝██╔════╝██╔══██╗██║     ██╔════╝
    ██████╔╝ ╚████╔╝ █████╗  ██║   ██║██╔██╗ ██║██║     █████╗  ██████╔╝██║     █████╗
    ██╔═══╝   ╚██╔╝  ██╔══╝  ██║   ██║██║╚██╗██║██║     ██╔══╝  ██╔══██╗██║     ██╔══╝
    ██║        ██║   ██║     ╚██████╔╝██║ ╚████║╚██████╗███████╗██████╔╝███████╗███████╗
    ╚═╝        ╚═╝   ╚═╝      ╚═════╝ ╚═╝  ╚═══╝ ╚═════╝╚══════╝╚═════╝ ╚══════╝╚══════╝

Provides the dict helpers.

Author:
    Nissar Chababy, @funilrys, contactTATAfunilrysTODTODcom

Special thanks:
    https://pyfunceble.github.io/#/special-thanks

Contributors:
    https://pyfunceble.github.io/#/contributors

Project link:
    https://github.com/funilrys/PyFunceble

Project documentation:
    https://docs.pyfunceble.com

Project homepage:
    https://pyfunceble.github.io/

License:
::


    Copyright 2017, 2018, 2019, 2020, 2022, 2023, 2024, 2025 Nissar Chababy

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        https://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
"""

import copy
import json
from json import decoder, dump, dumps, loads
from typing import Any, List, Optional, Union

from yaml import dump as yaml_dump
from yaml import safe_load as yaml_load

from PyFunceble.helpers.file import FileHelper


class DictHelper:
    """
    Simplify some :code:`dict` manipulation.

    :param dict main: The main :code:`dict` to work with.
    :raise TypeError: When :code:`main` is not a dict nor a list (tolerated).
    """

    _subject: Optional[Union[Any, dict]] = None

    def __init__(self, subject: Optional[Union[Any, dict]] = None) -> None:
        if subject is not None:
            self.subject = subject

    @property
    def subject(self) -> Optional[Union[Any, dict]]:
        """
        Provides the current state of the :code:`_subject` attribute.
        """

        return self._subject

    @subject.setter
    def subject(self, value: Any) -> None:
        """
        Sets the subject to work with.

        :param value:
            The value to set.
        """

        self._subject = copy.deepcopy(value)

    def set_subject(self, value: Any) -> "DictHelper":
        """
        Sets the subject to work with.

        :param value:
            The value to set.
        """

        self.subject = value

        return self

    def has_same_keys_as(self, to_check: dict, loop: bool = False) -> bool:
        """
        Checks if keys are presents in both given :py:class:`dict`.

        :param to_check:
            The dict to compare with.
        :param loop:
            DO NOT USE, only used to tell us wen to return the list of dataset
            or the final result.
        """

        result = []

        for key, value in to_check.items():
            if key in self.subject:
                if isinstance(value, dict) and isinstance(self.subject[key], dict):
                    result.extend(
                        DictHelper(self.subject[key]).has_same_keys_as(value, loop=True)
                    )
                else:
                    result.append(True)
            else:
                result.append(False)
        if loop:
            return result
        return False not in result

    def remove_key(
        self, key_to_remove: Union[str, List[str]]
    ) -> Optional[Union[Any, dict]]:
        """
        Remove a given key from a given dictionary.

        :param key_to_remove: The key(s) to delete.

        :return: The dict without the given key(s).
        """

        if isinstance(self.subject, dict):
            if isinstance(key_to_remove, list):
                for key in key_to_remove:
                    self.remove_key(key)
            else:
                try:
                    del self.subject[key_to_remove]
                except KeyError:
                    pass
        return self.subject

    def rename_key(self, key_to_rename: dict, strict: bool = True) -> dict:
        """
        Rename the given keys from the given dictionary.

        :param key_to_rename:
            The key(s) to rename.

            Expected format: :code:`{old:new}`

        :param strict:
            Tell us if we have to rename the exact index or
            the index which looks like the given key(s)

        :return: The well formatted dict.
        """

        if isinstance(self.subject, dict) and isinstance(key_to_rename, dict):
            for old, new in key_to_rename.items():
                if strict:
                    if old in self.subject:
                        self.subject[new] = self.subject.pop(old)
                else:
                    to_rename = {}

                    for index in self.subject:
                        if old in index:
                            to_rename.update({index: new[:-1] + index.split(old)[-1]})

                    self.subject = DictHelper(self.subject).rename_key(to_rename, True)
        return self.subject

    def to_json_file(
        self,
        file_path: str,
        ensure_ascii: bool = False,
        indent: int = 4,
        sort_keys: bool = True,
        encoding: str = "utf-8",
        own_class: Optional[json.JSONEncoder] = None,
    ) -> None:
        """
        Converts the given :code:`dict` to JSON and save the result
        into a given file path.

        :param file_path: The file path.
        :param ensure_ascii: Avoids unicode.
        :param indent: The indentation to apply.
        :param sortkeys: Sorts the keys.
        :param encoding: The encoding to apply.
        :param own_class: A class to use for the conversion to json.
        """

        with open(file_path, "w", encoding=encoding) as file_stream:
            dump(
                self.subject,
                file_stream,
                ensure_ascii=ensure_ascii,
                indent=indent,
                sort_keys=sort_keys,
                cls=own_class,
            )

    @staticmethod
    def from_json_file(
        file_path: str, encoding: str = "utf-8", return_dict_on_error: bool = True
    ) -> Optional[Union[List[Any], dict]]:
        """
        Reads the given file path and convert it's content to
        dict/list (tolarated).

        :param file_path: The file path.
        :param return_dict_on_error: Return a dict instead of a NoneType.
        :parma encoding: The encoding to use.
        """

        try:
            return loads(FileHelper(path=file_path).read(encoding=encoding))
        except (decoder.JSONDecodeError, TypeError):
            return None if not return_dict_on_error else {}

    def to_json(
        self,
        ensure_ascii: bool = False,
        indent: int = 4,
        sort_keys: bool = True,
        own_class: Optional[json.JSONEncoder] = None,
    ) -> str:
        """
        Converts a given dict to JSON and return the json string.

        :param ensure_ascii: Avoids unicode.
        :param indent: The indentation to apply.
        :param sort_keys: Sort the keys.
        :param own_class: A class to use for the conversion to json.
        """

        return dumps(
            self.subject,
            ensure_ascii=ensure_ascii,
            indent=indent,
            sort_keys=sort_keys,
            cls=own_class,
        )

    @staticmethod
    def from_json(
        json_str: str, return_dict_on_error: bool = True
    ) -> Optional[Union[List[Any], dict]]:
        """
        Converts a given JSON string to dict/list.

        :param json_str: The JSON string ot convert.
        :param return_dict_on_error:
            Returns a :py:class:`dict` instead of a :py:class:`None`.
        """

        try:
            return loads(json_str)
        except (decoder.JSONDecodeError, TypeError):
            return None if not return_dict_on_error else {}

    @staticmethod
    def from_yaml_file(
        file_path: str, encoding: str = "utf-8"
    ) -> Union[List[Any], dict]:
        """
        Converts a given YAML formatted file, into dict/list.

        :param file_path: The file path.
        :param encoding: The encoding to use.
        """

        with open(file_path, "r", encoding=encoding) as file_stream:
            data = yaml_load(file_stream)

        return data

    def to_yaml_file(
        self,
        file_path: str,
        encoding: str = "utf-8",
        default_flow_style: bool = False,
        indent: int = 4,
        allow_unicode: bool = True,
        sort_keys: bool = True,
    ) -> None:
        """
        Converts the given dict/list to YAML and save the result into a file.

        :param file_path: The file path.
        :param encoding: The encoding.
        :param default_flow_style: Uses the default flow style.
        :param indent: The indentation to apply.
        :param allow_unicode: Allows the  decoding of unicode chars.
        :param sort_keys: Sorts the keys.
        """

        with open(file_path, "w", encoding=encoding) as file_stream:
            yaml_dump(
                self.subject,
                stream=file_stream,
                default_flow_style=default_flow_style,
                indent=indent,
                allow_unicode=allow_unicode,
                encoding=encoding,
                sort_keys=sort_keys,
            )

    @staticmethod
    def from_yaml(yaml_str) -> Union[List[Any], dict]:
        """
        Converts the given YAML string to dict/list.

        :param str yaml_str: The YAML string to convert.
        """

        return yaml_load(yaml_str)

    def to_yaml(
        self,
        encoding: str = "utf-8",
        default_flow_style: bool = False,
        indent: int = 4,
        allow_unicode: bool = True,
        sort_keys: bool = True,
    ) -> str:
        """
        Converts the given dict/list to the YAML format and return
        the result.

        :param str encoding: The encoding to use.
        :param bool default_flow_style: Uses the default flow style.
        :param int indent: The indentation to apply.
        :param bool allow_unicode: Allows the decoding of unicode chars.
        :param bool sort_keys: Sorts the keys.

        :rtype: dict|list
        """

        return yaml_dump(
            self.subject,
            default_flow_style=default_flow_style,
            indent=indent,
            allow_unicode=allow_unicode,
            encoding=encoding,
            sort_keys=sort_keys,
        ).decode()

    def flatten(
        self,
        *,
        separator: str = ".",
        previous: Optional[str] = None,
        data: Optional[Any] = None,
    ) -> dict:
        """
        Flatten the current dictionary.

        :param separator:
            The separator to apply.
        :param previous:
            The previous key we are working with.
        :param data:
            The data to work with. If not given, we fallback to :code:`self.subject`.
        """

        if data is None:
            data = self.subject

        result = {}

        if isinstance(data, dict):
            for key, value in data.items():
                for yek, eulav in (
                    DictHelper(value).flatten(separator=separator, previous=key).items()
                ):
                    if previous is not None:
                        result[f"{previous}{separator}{yek}"] = eulav
                    else:
                        result[yek] = eulav
        else:
            if previous:
                result[previous] = data
            else:
                result[separator] = data

        return result

    def unflatten(self, *, separator: str = ".", data: Optional[Any] = None):
        """
        Unflatten a previously flatten dictionary.

        :param separator:
            The separator to split.
        """

        if data is None:
            data = self.subject

        result = {}

        for key, value in data.items():
            local_result = result

            if separator in key:
                splitted_sep = key.replace(separator + separator, separator).split(
                    separator
                )

                for yek in splitted_sep[:-1]:
                    if yek not in local_result:
                        local_result[yek] = {}

                    local_result = local_result[yek]
                local_result[splitted_sep[-1]] = value
            else:
                local_result[key] = value

        return result