4.4. Dataclass Define Nested

4.4.1. SetUp

>>> from dataclasses import dataclass
>>> from typing import Self

4.4.2. Relation to Objects

>>> @dataclass
... class Group:
...     gid: int
...     name: str
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     groups: list[Group]

Usage:

>>> mark = User('Mark', 'Watney', groups=[
...     Group(gid=1, name='users'),
...     Group(gid=2, name='staff'),
...     Group(gid=3, name='admins')])

4.4.3. Relation to Self

  • Note, that there are None default friends

  • Using an empty list [] as a default value will not work

  • Self is available since Python 3.11

  • We will cover this topic later

Import:

>>> from typing import Self

Define class:

>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     friends: list[Self] = None

Use:

>>> mark = User('Mark', 'Watney', friends=[
...     User('Melissa', 'Lewis'),
...     User('Rick', 'Martinez'),
...     User('Beth', 'Johanssen'),
...     User('Chris', 'Beck'),
...     User('Alex', 'Vogel')])

4.4.4. Assignments

Code 4.42. Solution
"""
* Assignment: Dataclass DefineRelations Syntax
* Complexity: easy
* Lines of code: 16 lines
* Time: 5 min

English:
    1. You received input data in JSON format from the API
    2. Using `dataclass` model `DATA`:
       a. Create class `Pet`
       b. Create class `Category`
       c. Create class `Tags`
    3. Model relations between classes
    4. Run doctests - all must succeed

Polish:
    1. Otrzymałeś z API dane wejściowe w formacie JSON
    2. Wykorzystując `dataclass` zamodeluj `DATA`:
       a. Stwórz klasę `Pet`
       b. Stwórz klasę `Category`
       c. Stwórz klasę `Tags`
    3. Zamodeluj relacje między klasami
    4. Uruchom doctesty - wszystkie muszą się powieść

References:
    [1]: https://petstore.swagger.io/#/pet/getPetById

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass
    >>> from dataclasses import is_dataclass
    >>> import json

    >>> assert isclass(Pet)
    >>> assert isclass(Category)
    >>> assert isclass(Tag)
    >>> assert is_dataclass(Pet)
    >>> assert is_dataclass(Category)
    >>> assert is_dataclass(Tag)

    >>> fields = {'id', 'category', 'name', 'photoUrls', 'tags', 'status'}
    >>> assert set(Pet.__dataclass_fields__.keys()) == fields, \
    f'Invalid fields, your fields should be: {fields}'

    >>> data = json.loads(DATA)
    >>> result = Pet(**data)
    >>> result.category = Category(**result.category)
    >>> result.tags = [Tag(**tag) for tag in result.tags]

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    Pet(id=0, category=Category(id=0, name='dogs'), name='doggie',
        photoUrls=['img/dogs/0.png'], tags=[Tag(id=0, name='dog'),
                                            Tag(id=1, name='hot-dog')],
        status='available')
"""

from dataclasses import dataclass


DATA = """
{
  "id": 0,
  "category": {
    "id": 0,
    "name": "dogs"
  },
  "name": "doggie",
  "photoUrls": [
    "img/dogs/0.png"
  ],
  "tags": [
    {
      "id": 0,
      "name": "dog"
    },
    {
      "id": 1,
      "name": "hot-dog"
    }
  ],
  "status": "available"
}
"""


# Using `dataclass` model `DATA`, create class `Category`
# type: type
@dataclass
class Category:
    ...


# Using `dataclass` model `DATA`, create class `Tag`
# type: type
@dataclass
class Tag:
    ...


# Using `dataclass` model `DATA`, create class `Pet`
# type: type
@dataclass
class Pet:
    ...


Code 4.43. Solution
"""
* Assignment: Dataclass Field Addressbook
* Complexity: easy
* Lines of code: 12 lines
* Time: 8 min

English:
    1. Model `DATA` using `dataclasses`
    2. Create class definition, fields and their types
    3. Do not write code converting `DATA` to your classes
    4. Run doctests - all must succeed

Polish:
    1. Zamodeluj `DATA` wykorzystując `dataclass`
    2. Stwórz definicję klas, pól i ich typów
    3. Nie pisz kodu konwertującego `DATA` do Twoich klas
    4. Uruchom doctesty - wszystkie muszą się powieść

Non-functional Requirements:
   * Use Python 3.10 syntax for Optionals, ie: `str | None`

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass
    >>> from dataclasses import is_dataclass
    >>> from typing import get_type_hints

    >>> assert isclass(User)
    >>> assert isclass(Address)
    >>> assert is_dataclass(User)
    >>> assert is_dataclass(Address)

    >>> user = get_type_hints(User)
    >>> address = get_type_hints(Address)

    >>> assert 'firstname' in user, \
    'Class User is missing field: firstname'
    >>> assert 'lastname' in user, \
    'Class User is missing field: lastname'
    >>> assert 'addresses' in user, \
    'Class User is missing field: addresses'
    >>> assert 'street' in address, \
    'Class Address is missing field: street'
    >>> assert 'city' in address, \
    'Class Address is missing field: city'
    >>> assert 'post_code' in address, \
    'Class Address is missing field: post_code'
    >>> assert 'region' in address, \
    'Class Address is missing field: region'
    >>> assert 'country' in address, \
    'Class Address is missing field: country'
    >>> assert user['firstname'] is str, \
    'User.firstname has invalid type annotation, expected: str'
    >>> assert user['lastname'] is str, \
    'User.lastname has invalid type annotation, expected: str'
    >>> assert user['addresses'] == list[Address] | None, \
    'User.addresses has invalid type annotation, expected: list[Address] | None'
    >>> assert address['street'] == str | None, \
    'Address.street has invalid type annotation, expected: str | None'
    >>> assert address['city'] is str, \
    'Address.city has invalid type annotation, expected: str'
    >>> assert address['post_code'] is int, \
    'Address.post_code has invalid type annotation, expected: int'
    >>> assert address['region'] is str, \
    'Address.region has invalid type annotation, expected: str'
    >>> assert address['country'] is str, \
    'Address.country has invalid type annotation, expected: str'
"""
from dataclasses import dataclass


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

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

    {"firstname": "Rick", "lastname": "Martinez"},

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


# Model `DATA` using `dataclass`
# type: type
@dataclass
class Address:
    ...


# Model `DATA` using `dataclass`
# type: type
@dataclass
class User:
    ...


Code 4.44. Solution
"""
* Assignment: Dataclass DefineBasic DatabaseDump
* Complexity: medium
* Lines of code: 13 lines
* Time: 13 min

English:
    1. You received input data in JSON format from the API
       a. `str` fields: firstname, lastname, role, username, password, email,
       b. `date` field: birthday,
       c. `datetime` field: last_login (optional),
       d. `bool` fields: is_active, is_staff, is_superuser,
       e. `list[dict]` field: user_permissions
    2. Using `dataclass` model data as class `User`
    3. Do not create additional classes to represent `permission` field,
       leave it as `list[dict]`
    4. Note, that fields order is important for tests to pass
    5. Run doctests - all must succeed

Polish:
    1. Otrzymałeś z API dane wejściowe w formacie JSON
       a. pola `str`: firstname, lastname, role, username, password, email,
       b. pole `date`: birthday,
       c. pole `datetime`: last_login (opcjonalne),
       d. pola `bool`: is_active, is_staff, is_superuser,
       e. pola `list[dict]`: user_permissions
    2. Wykorzystując `dataclass` zamodeluj dane za pomocą klasy `User`
    3. Nie twórz dodatkowych klas do reprezentacji pola `permission`,
       niech zostanie jako `list[dict]`
    4. Zwróć uwagę, że kolejność pól ma znaczenie aby testy przechodziły
    5. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `pk` - Primary Key (unique identifier, an ID in database)
    * `model` - package name with name of a class
    * `datetime | None`
    * `date`
    * `list[dict]`

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isclass
    >>> from dataclasses import is_dataclass
    >>> from pprint import pprint

    >>> assert isclass(User)
    >>> assert is_dataclass(User)

    >>> attributes = User.__dataclass_fields__.keys()
    >>> list(attributes)  # doctest: +NORMALIZE_WHITESPACE
    ['firstname', 'lastname', 'role', 'username', 'password', 'email', 'birthday',
     'last_login', 'is_active', 'is_staff', 'is_superuser', 'user_permissions']

    >>> pprint(User.__annotations__, sort_dicts=False)
    {'firstname': <class 'str'>,
     'lastname': <class 'str'>,
     'role': <class 'str'>,
     'username': <class 'str'>,
     'password': <class 'str'>,
     'email': <class 'str'>,
     'birthday': <class 'datetime.date'>,
     'last_login': datetime.datetime | None,
     'is_active': <class 'bool'>,
     'is_staff': <class 'bool'>,
     'is_superuser': <class 'bool'>,
     'user_permissions': list[dict]}

    >>> result = [User(**user['fields']) for user in json.loads(DATA)]

    >>> pprint(result[0])  # doctest: +ELLIPSIS
    User(firstname='Melissa',
         lastname='Lewis',
         role='commander',
         username='mlewis',
         password='pbkdf2_sha256$120000$gvEBNiCeTrYa0$5C+NiCeTrYsha1PHog...=',
         email='mlewis@nasa.gov',
         birthday='1995-07-15',
         last_login='1970-01-01T00:00:00.000+00:00',
         is_active=True,
         is_staff=True,
         is_superuser=False,
         user_permissions=[{'eclss': ['add', 'modify', 'view']},
                           {'communication': ['add', 'modify', 'view']},
                           {'medical': ['add', 'modify', 'view']},
                           {'science': ['add', 'modify', 'view']}])
"""

import json
from dataclasses import dataclass
from datetime import date, datetime


DATA = ('[{"model":"authorization.user","pk":1,"fields":{"firstname":"Meli'
        'ssa","lastname":"Lewis","role":"commander","username":"mlewis","p'
        'assword":"pbkdf2_sha256$120000$gvEBNiCeTrYa0$5C+NiCeTrYsha1PHogqv'
        'XNiCeTrY0CRSLYYAA90=","email":"mlewis@nasa.gov","birthday":"1995-'
        '07-15","last_login":"1970-01-01T00:00:00.000+00:00","is_active":t'
        'rue,"is_staff":true,"is_superuser":false,"user_permissions":[{"ec'
        'lss":["add","modify","view"]},{"communication":["add","modify","v'
        'iew"]},{"medical":["add","modify","view"]},{"science":["add","mod'
        'ify","view"]}]}},{"model":"authorization.user","pk":2,"fields":{"'
        'firstname":"Rick","lastname":"Martinez","role":"pilot","username"'
        ':"rmartinez","password":"pbkdf2_sha256$120000$aXNiCeTrY$UfCJrBh/q'
        'hXohNiCeTrYH8nsdANiCeTrYnShs9M/c=","birthday":"1996-01-21","last_'
        'login":null,"email":"rmartinez@nasa.gov","is_active":true,"is_sta'
        'ff":true,"is_superuser":false,"user_permissions":[{"communication'
        '":["add","view"]},{"eclss":["add","modify","view"]},{"science":["'
        'add","modify","view"]}]}},{"model":"authorization.user","pk":3,"f'
        'ields":{"firstname":"Alex","lastname":"Vogel","role":"chemist","u'
        'sername":"avogel","password":"pbkdf2_sha256$120000$eUNiCeTrYHoh$X'
        '32NiCeTrYZOWFdBcVT1l3NiCeTrY4WJVhr+cKg=","email":"avogel@esa.int"'
        ',"birthday":"1994-11-15","last_login":null,"is_active":true,"is_s'
        'taff":true,"is_superuser":false,"user_permissions":[{"eclss":["ad'
        'd","modify","view"]},{"communication":["add","modify","view"]},{"'
        'medical":["add","modify","view"]},{"science":["add","modify","vie'
        'w"]}]}},{"model":"authorization.user","pk":4,"fields":{"firstname'
        '":"Chris","lastname":"Beck","role":"crew-medical-officer","userna'
        'me":"cbeck","password":"pbkdf2_sha256$120000$3G0RNiCeTrYlaV1$mVb6'
        '2WNiCeTrYQ9aYzTsSh74NiCeTrY2+c9/M=","email":"cbeck@nasa.gov","bir'
        'thday":"1999-08-02","last_login":"1970-01-01T00:00:00.000+00:00",'
        '"is_active":true,"is_staff":true,"is_superuser":false,"user_permi'
        'ssions":[{"communication":["add","view"]},{"medical":["add","modi'
        'fy","view"]},{"science":["add","modify","view"]}]}},{"model":"aut'
        'horization.user","pk":5,"fields":{"firstname":"Beth","lastname":"'
        'Johanssen","role":"sysop","username":"bjohanssen","password":"pbk'
        'df2_sha256$120000$QmSNiCeTrYBv$Nt1jhVyacNiCeTrYSuKzJ//WdyjlNiCeTr'
        'YYZ3sB1r0g=","email":"bjohanssen@nasa.gov","birthday":"2006-05-09'
        '","last_login":null,"is_active":true,"is_staff":true,"is_superuse'
        'r":false,"user_permissions":[{"communication":["add","view"]},{"s'
        'cience":["add","modify","view"]}]}},{"model":"authorization.user"'
        ',"pk":6,"fields":{"firstname":"Mark","lastname":"Watney","role":"'
        'botanist","username":"mwatney","password":"pbkdf2_sha256$120000$b'
        'xS4dNiCeTrY1n$Y8NiCeTrYRMa5bNJhTFjNiCeTrYp5swZni2RQbs=","email":"'
        'mwatney@nasa.gov","birthday":"1994-10-12","last_login":null,"is_a'
        'ctive":true,"is_staff":true,"is_superuser":false,"user_permission'
        's":[{"communication":["add","modify","view"]},{"science":["add","'
        'modify","view"]}]}}]')

# Convert JSON to Python dict
data = json.loads(DATA)


# Using `dataclass` model data as class `User`
# a. `str` fields: firstname, lastname, role, username, password, email,
# b. `date` field: birthday,
# c. `datetime` field: last_login (optional),
# c. `bool` fields: is_active, is_staff, is_superuser,
# d. `list[dict]` field: user_permissions
# Leave `permission` attribute as `list[dict]`
# Note, that fields order is important for tests to pass
# type: type
class User:
    ...