11.5. FP Apply Recap

11.5.1. Assignments

# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Functional Recap Json2Dataclass
# - Difficulty: medium
# - Lines: 7
# - Minutes: 8

# %% English
# 1. In `DATA` we have two classes
# 2. Model data using classes and relations
# 3. Create instances dynamically based on `DATA`
# 4. Run doctests - all must succeed

# %% Polish
# 1. W `DATA` mamy dwie klasy
# 2. Zamodeluj problem wykorzystując klasy i relacje między nimi
# 3. Twórz instancje dynamicznie na podstawie `DATA`
# 4. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `dict.get()`
# - `dict.values()`
# - `map()`
# - `starmap()`
# - `tuple()`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from pprint import pprint

>>> result = list(result)
>>> assert type(result) is list
>>> assert all(type(user) is User for user in result)

>>> assert all(type(addr) is Address
...            for user in result
...            for addr in user.addresses)

>>> pprint(result)  # doctest: +NORMALIZE_WHITESPACE
[User(firstname='Mark',
      lastname='Watney',
      addresses=(Address(street='2101 E NASA Pkwy',
                         city='Houston',
                         postcode=77058,
                         region='Texas',
                         country='USA'),
                 Address(street='',
                         city='Kennedy Space Center',
                         postcode=32899,
                         region='Florida',
                         country='USA'))),
 User(firstname='Melissa',
      lastname='Lewis',
      addresses=(Address(street='4800 Oak Grove Dr',
                         city='Pasadena',
                         postcode=91109,
                         region='California',
                         country='USA'),
                 Address(street='2825 E Ave P',
                         city='Palmdale',
                         postcode=93550,
                         region='California',
                         country='USA'))),
 User(firstname='Rick', lastname='Martinez', addresses=()),
 User(firstname='Alex',
      lastname='Vogel',
      addresses=(Address(street='Linder Hoehe',
                         city='Cologne',
                         postcode=51147,
                         region='North Rhine-Westphalia',
                         country='Germany'),))]
"""

from dataclasses import dataclass
from itertools import starmap


DATA = [
    {"firstname": "Mark", "lastname": "Watney", "addresses": [
        {"street": "2101 E NASA Pkwy",
         "city": "Houston",
         "postcode": 77058,
         "region": "Texas",
         "country": "USA"},
        {"street": "",
         "city": "Kennedy Space Center",
         "postcode": 32899,
         "region": "Florida",
         "country": "USA"}]},

    {"firstname": "Melissa", "lastname": "Lewis", "addresses": [
        {"street": "4800 Oak Grove Dr",
         "city": "Pasadena",
         "postcode": 91109,
         "region": "California",
         "country": "USA"},
        {"street": "2825 E Ave P",
         "city": "Palmdale",
         "postcode": 93550,
         "region": "California",
         "country": "USA"}]},

    {"firstname": "Rick", "lastname": "Martinez", "addresses": []},

    {"firstname": "Alex", "lastname": "Vogel", "addresses": [
        {"street": "Linder Hoehe",
         "city": "Cologne",
         "postcode": 51147,
         "region": "North Rhine-Westphalia",
         "country": "Germany"}]}
]


@dataclass
class Address:
    street: str
    city: str
    postcode: int
    region: str
    country: str


@dataclass
class User:
    firstname: str
    lastname: str
    addresses: tuple[Address, ...]


# Iterate over `DATA` and create instances
# type: map[User]
result = ...


# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Functional Recap Json2Csv
# - Difficulty: medium
# - Lines: 10
# - Minutes: 13

# %% English
# 1. Write data with relations to CSV format
# 2. Convert `DATA` to `result: list[dict[str,str]]`
# 3. Non-functional requirements:
#    - Use `,` to separate fields
#    - Use `;` to separate instances
# 4. Contact has zero or many addresses
# 5. Run doctests - all must succeed

# %% Polish
# 1. Zapisz dane relacyjne do formatu CSV
# 2. Przekonwertuj `DATA` do `result: list[dict[str,str]]`
# 3. Wymagania niefunkcjonalne:
#    - Użyj `,` do oddzielenia pól
#    - Użyj `;` do oddzielenia instancji
# 4. Kontakt ma zero lub wiele adresów
# 5. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `map()`
# - `dict()`
# - `dict.values()`
# - `dict.get()`
# - `str.join()`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from pprint import pprint

>>> result = list(result)
>>> assert type(result) is list
>>> assert len(result) > 0
>>> assert all(type(x) is dict for x in result)

>>> pprint(result, width=112, sort_dicts=False)  # doctest: +NORMALIZE_WHITESPACE
[{'firstname': 'Mark',
  'lastname': 'Watney',
  'addresses': '2101 E NASA Pkwy,Houston,77058,Texas,USA;,Kennedy Space Center,32899,Florida,USA'},
 {'firstname': 'Melissa',
  'lastname': 'Lewis',
  'addresses': '4800 Oak Grove Dr,Pasadena,91109,California,USA;2825 E Ave P,Palmdale,93550,California,USA'},
 {'firstname': 'Rick', 'lastname': 'Martinez', 'addresses': ''},
 {'firstname': 'Alex',
  'lastname': 'Vogel',
  'addresses': 'Linder Hoehe,Cologne,51147,North Rhine-Westphalia,Germany'}]
"""

DATA = [
    {"firstname": "Mark", "lastname": "Watney", "addresses": [
        {"street": "2101 E NASA Pkwy",
         "city": "Houston",
         "postcode": 77058,
         "region": "Texas",
         "country": "USA"},
        {"street": "",
         "city": "Kennedy Space Center",
         "postcode": 32899,
         "region": "Florida",
         "country": "USA"}]},

    {"firstname": "Melissa", "lastname": "Lewis", "addresses": [
        {"street": "4800 Oak Grove Dr",
         "city": "Pasadena",
         "postcode": 91109,
         "region": "California",
         "country": "USA"},
        {"street": "2825 E Ave P",
         "city": "Palmdale",
         "postcode": 93550,
         "region": "California",
         "country": "USA"}]},

    {"firstname": "Rick", "lastname": "Martinez", "addresses": []},

    {"firstname": "Alex", "lastname": "Vogel", "addresses": [
        {"street": "Linder Hoehe",
         "city": "Cologne",
         "postcode": 51147,
         "region": "North Rhine-Westphalia",
         "country": "Germany"}]}
]

# Flatten data, each address field prefixed with address and number
# type: map[dict]
result = ...


# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Functional Recap FlattenDicts
# - Difficulty: hard
# - Lines: 13
# - Minutes: 13

# %% English
# 1. Convert `DATA` to format with one column per each attrbute for example:
#    - `group1_gid`, `group2_gid`,
#    - `group1_name`, `group2_name`
# 2. Note, that enumeration starts with one
# 3. Collect data to `result: map`
# 4. Run doctests - all must succeed

# %% Polish
# 1. Przekonweruj `DATA` do formatu z jedną kolumną dla każdego atrybutu, np:
#    - `group1_gid`, `group2_gid`,
#    - `group1_name`, `group2_name`
# 2. Zwróć uwagę, że enumeracja zaczyna się od jeden
# 3. Zbierz dane do `result: map`
# 4. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `itertools.starmap()`
# - `functools.reduce()`
# - `operator.or_`
# - `dict.get()`
# - `enumerate(iterable, start)`
# - `reduce(func, iterable, {})`
# - `dict | dict`
# - `map()`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from pprint import pprint

>>> result = list(result)
>>> assert type(result) is list
>>> assert len(result) > 0
>>> assert all(type(x) is dict for x in result)

>>> pprint(result, width=30, sort_dicts=False)
[{'firstname': 'Mark',
  'lastname': 'Watney',
  'group1_gid': 1,
  'group1_name': 'staff'},
 {'firstname': 'Melissa',
  'lastname': 'Lewis',
  'group1_gid': 1,
  'group1_name': 'staff',
  'group2_gid': 2,
  'group2_name': 'admins'},
 {'firstname': 'Rick',
  'lastname': 'Martinez'}]
"""

from itertools import starmap
from functools import reduce
from operator import or_


DATA = [
    {"firstname": "Mark", "lastname": "Watney", "groups": [
        {"gid": 1, "name": "staff"}]},

    {"firstname": "Melissa", "lastname": "Lewis", "groups": [
        {"gid": 1, "name": "staff"},
        {"gid": 2, "name": "admins"}]},

    {"firstname": "Rick", "lastname": "Martinez", "groups": []},
]


# Flatten data, each mission field prefixed with mission and number
# type: map[dict]
def convert(user):
    return ...

result = map(convert, DATA)


# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Functional Recap FlattenClasses
# - Difficulty: hard
# - Lines: 15
# - Minutes: 13

# %% English
# 1. Convert `DATA` to format with one column per each attrbute for example:
#    - `Group1_year`, `Group2_year`,
#    - `Group1_name`, `Group2_name`
# 2. Note, that enumeration starts with one
# 3. Run doctests - all must succeed

# %% Polish
# 1. Przekonweruj `DATA` do formatu z jedną kolumną dla każdego atrybutu, np:
#    - `Group1_year`, `Group2_year`,
#    - `Group1_name`, `Group2_name`
# 2. Zwróć uwagę, że enumeracja zaczyna się od jeden
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Hints
# - `vars(obj)` -> `dict`

# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from pprint import pprint

>>> result = list(result)
>>> assert type(result) is list
>>> assert len(result) > 0
>>> assert all(type(x) is dict for x in result)

>>> pprint(result, width=30, sort_dicts=False)
[{'firstname': 'Mark',
  'lastname': 'Watney',
  'group1_gid': 1,
  'group1_name': 'staff'},
 {'firstname': 'Melissa',
  'lastname': 'Lewis',
  'group1_gid': 1,
  'group1_name': 'staff',
  'group2_gid': 2,
  'group2_name': 'admins'},
 {'firstname': 'Rick',
  'lastname': 'Martinez'}]
"""

from itertools import starmap
from functools import reduce
from operator import or_


class User:
    def __init__(self, firstname, lastname, groups=()):
        self.firstname = firstname
        self.lastname = lastname
        self.groups = list(groups)


class Group:
    def __init__(self, gid, name):
        self.gid = gid
        self.name = name

DATA = [
    User('Mark', 'Watney', groups=[
        Group(gid=1, name='staff')]),

    User('Melissa', 'Lewis', groups=[
        Group(gid=1, name='staff'),
        Group(gid=2, name='admins')]),

    User('Rick', 'Martinez'),
]


# Convert DATA
# type: map[dict]
def flat(i, groupobj):
    group = groupobj
    gid = group.get('gid')
    name = group.get('name')
    return {f'group{i}_gid': gid, f'group{i}_name': name}

def flatten(groups):
    dicts = starmap(flat, enumerate(groups, start=1))
    return reduce(or_, dicts, {})

def convert(userobj):
    user = userobj
    firstname = user.get('firstname')
    lastname = user.get('lastname')
    groups = flatten(user.get('groups'))
    return {'firstname': firstname, 'lastname': lastname} | groups

result = map(convert, DATA)