Skip to content

Tools

Helper modules for Chopin

Dates

Date range utilitaries.

parse_release_date(date)

Parse the release date depending on the level of detail available.

Parameters:

Name Type Description Default
date str

An input date, as a string

required

Returns:

Type Description
date

A formatted date, with the datetime library.

Source code in chopin/tools/dates.py
def parse_release_date(date: str) -> date:
    """Parse the release date depending on the level of detail available.

    Args:
        date: An input date, as a string

    Returns:
        A formatted date, with the datetime library.
    """
    if not date:
        return datetime(1970, 1, 1, 0, 0).date()
    match len(date):
        case 4:
            _format = "%Y"
        case 7:
            _format = "%Y-%m"
        case 10:
            _format = "%Y-%m-%d"
        case 19:
            _format = "%Y-%m-%dT%M:%S:%f"
        case _:
            raise ValueError(f"Bad release date format: {date}")
    return datetime.strptime(date, _format).date()

read_date(date)

Read a date from a string tuple.

Parameters:

Name Type Description Default
date tuple[str | None, str | None] | None

The date to parse

required

Returns:

Type Description
ReleaseRange | None

A parsed date (if it exists), e.g. a range of two datetime objects.

Examples:

>>> read_date('10/01/2023', '10/02/2023')
(datetime.datetime(2023, 1, 10, 0, 0), datetime.datetime(2023, 2, 10, 0, 0))
>>> read_date('10/01/2023', )
(datetime.datetime(2023, 1, 10, 0, 0), datetime.datetime.now()
Source code in chopin/tools/dates.py
def read_date(date: tuple[str | None, str | None] | None) -> ReleaseRange | None:
    """Read a date from  a string tuple.

    Args:
        date: The date to parse

    Returns:
        A parsed date (if it exists), e.g. a range of two datetime objects.

    Examples:
        >>> read_date('10/01/2023', '10/02/2023')
        (datetime.datetime(2023, 1, 10, 0, 0), datetime.datetime(2023, 2, 10, 0, 0))
        >>> read_date('10/01/2023', )
        (datetime.datetime(2023, 1, 10, 0, 0), datetime.datetime.now()
    """
    _format = "%d/%m/%Y"
    match date:
        case None:
            return date
        case (str(), str()):
            return datetime.strptime(date[0], _format), datetime.strptime(date[1], _format)
        case (str(), None):
            return datetime.strptime(date[0], _format), datetime.now()
        case (None, str()):
            return datetime.strptime("01/01/1900", _format), datetime.strptime(date[1], _format)

Dictionaries

Utilities to help with dictionaries.

flatten_dict(dictionary)

Flatten a dictionary with potential nested dictionaries, into a single dictionary.

Example

{'my_key': 'my_value',
 'a_nested_dict':
    {'my_key': 'my_value',
     'version': '1'
     }
}
will become:

{'my_key': 'my_value',
 'a_nested_dict.my_key' : 'my_value',
 'a_nested_dict.version': '1'
}
Source code in chopin/tools/dictionaries.py
def flatten_dict(dictionary: dict) -> dict:
    """Flatten a dictionary with potential nested dictionaries, into a single dictionary.

    !!! example
        ```
        {'my_key': 'my_value',
         'a_nested_dict':
            {'my_key': 'my_value',
             'version': '1'
             }
        }
        ```
        will become:

        ```
        {'my_key': 'my_value',
         'a_nested_dict.my_key' : 'my_value',
         'a_nested_dict.version': '1'
        }
        ```
    """
    final_dict = {}
    for key, value in dictionary.items():
        if isinstance(value, dict):
            for sub_key, sub_value in value.items():
                final_dict[f"{key}.{sub_key}"] = sub_value
        else:
            final_dict[key] = value
    return final_dict

Strings

Utilites to help deal with strings.

decode(encoded_string, alphabet=BASE62)

Decode a Base X encoded string into the number.

Parameters:

Name Type Description Default
encoded_string

The encoded string

required
alphabet

Alphabet to use for decoding

BASE62

Returns:

Type Description
str

The decoded string

Source code in chopin/tools/strings.py
def decode(encoded_string, alphabet=BASE62) -> str:
    """Decode a Base X encoded string into the number.

    Args:
        encoded_string: The encoded string
        alphabet: Alphabet to use for decoding

    Returns:
        The decoded string
    """
    base = len(alphabet)
    strlen = len(encoded_string)
    num = 0

    idx = 0
    for char in encoded_string:
        power = strlen - (idx + 1)
        num += alphabet.index(char) * (base**power)
        idx += 1

    decoded = "".join(chr((num >> 8 * (len(BASE62) - byte - 1)) & 0xFF) for byte in range(len(BASE62)))
    return decoded

Parse a playlist link and returns the playlist URI. The playlist URI is later used to query the Spotify API.

Example

https://open.spotify.com/playlist/37i9dQZF1DWWv8B5EWK7bn?si=8d52c3fef8d74064 becomes 37i9dQZF1DWWv8B5EWK7bn

Parameters:

Name Type Description Default
playlist_link str

https link to a Spotify playlist.

required

Returns:

Type Description
str

The playlist URI

Source code in chopin/tools/strings.py
def extract_uri_from_playlist_link(playlist_link: str) -> str:
    """Parse a playlist link and returns the playlist URI. The playlist URI is later used to query the Spotify API.

    ??? example
        `https://open.spotify.com/playlist/37i9dQZF1DWWv8B5EWK7bn?si=8d52c3fef8d74064` becomes `37i9dQZF1DWWv8B5EWK7bn`

    Args:
        playlist_link: https link to a Spotify playlist.

    Returns:
        The playlist URI
    """
    pattern = r"playlist/([a-zA-Z0-9]+)\?"
    match = re.search(pattern, playlist_link)
    if match:
        return match.group(1)
    return ""

match_strings(strings)

Check if all strings match.

They match if their lowercase, unicode stripped-off characters versions are similar.

Parameters:

Name Type Description Default
strings list[str]

A list of strings to match

required

Returns:

Type Description
bool

True if all strings are the same (minus lowercase and unicode special character differences)

Source code in chopin/tools/strings.py
def match_strings(strings: list[str]) -> bool:
    """Check if all strings match.

    They match if their lowercase, unicode stripped-off characters versions are similar.

    Args:
        strings: A list of strings to match

    Returns:
        True if all strings are the same (minus lowercase and unicode special character differences)
    """

    def _normalize_string(_string: str) -> str:
        return "".join(c for c in unicodedata.normalize("NFD", _string.lower()) if unicodedata.category(c) != "Mn")

    if len(strings) < 2:
        return True
    target = _normalize_string(strings[0])
    return all([_normalize_string(s) == target for s in strings[1:]])

owner_is_spotify(uri)

Test if the given URI is owned by spotify.

Parameters:

Name Type Description Default
uri str

URI to test

required

Returns:

Type Description
bool

True if 'spotify' is the owner of the URI item.

Source code in chopin/tools/strings.py
def owner_is_spotify(uri: str) -> bool:
    """Test if the given URI is owned by spotify.

    Args:
        uri: URI to test

    Returns:
        True if 'spotify' is the owner of the URI item.
    """
    decoded = "".join([char for char in decode(uri) if char.isalnum()])
    return decoded.startswith("format")

simplify_string(text)

Simplify a string: lowercase, and no emojis.

Source code in chopin/tools/strings.py
def simplify_string(text: str) -> str:
    """Simplify a string: lowercase, and no emojis."""
    text = emoji.replace_emoji(text)
    text = text.lower()
    text = text.rstrip(" ")
    text = text.lstrip(" ")
    text = text.replace("'", "")
    text = text.replace(" ", "")
    text = text.replace("&", "_")
    return text