4.3. Dataclass Relations

4.3.1. SetUp

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

4.3.2. Composition

>>> @dataclass
... class Group:
...     gid: int
...     name: str
>>>
>>>
>>> @dataclass
... class User:
...     firstname: str
...     lastname: str
...     group: Group

Usage:

>>> mark = User('Mark', 'Watney', group=Group(gid=1, name='users'))

4.3.3. Aggregation

>>> @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.3.4. Forward Reference

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

Usage:

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

4.3.5. 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: Dataclass Relations Syntax
# - Difficulty: easy
# - Lines: 7
# - Minutes: 2

# %% English
# 1. Use Dataclass to define class `Point` with attributes:
#    - `x: int` with default value `0`
#    - `y: int` with default value `0`
# 2. Use Dataclass to define class `Path` with attributes:
#    - `points: list[Point]`
# 3. Run doctests - all must succeed

# %% Polish
# 1. Użyj Dataclass do zdefiniowania klasy `Point` z atrybutami:
#    - `x: int` z domyślną wartością `0`
#    - `y: int` z domyślną wartością `0`
# 2. Użyj Dataclass do zdefiniowania klasy `Path` z atrybutami:
#    - `points: list[Point]`
# 3. Uruchom doctesty - wszystkie muszą się powieść

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

>>> from inspect import isclass

>>> assert isclass(Point)
>>> assert isclass(Path)
>>> assert hasattr(Point, 'x')
>>> assert hasattr(Point, 'y')

>>> Point()
Point(x=0, y=0)
>>> Point(x=0, y=0)
Point(x=0, y=0)
>>> Point(x=1, y=2)
Point(x=1, y=2)

>>> Path([Point(x=0, y=0),
...       Point(x=0, y=1),
...       Point(x=1, y=0)])
Path(points=[Point(x=0, y=0), Point(x=0, y=1), Point(x=1, y=0)])
"""

from dataclasses import dataclass


@dataclass
class Point:
    x: int = 0
    y: int = 0


@dataclass
class Path:
    ...


# %% 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: Dataclass Relations Syntax
# - Difficulty: easy
# - Lines: 10
# - Minutes: 3

# %% English
# 1. You received input data in JSON format from the API
# 2. Using `dataclass` model `DATA`:
#    - Create class `Pet`
#    - Create class `Category`
#    - 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`:
#    - Stwórz klasę `Pet`
#    - Stwórz klasę `Category`
#    - 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:
    id: int
    name: str


# Using `dataclass` model `DATA`, create class `Tag`
# type: type
@dataclass
class Tag:
    id: int
    name: str


# Using `dataclass` model `DATA`, create class `Pet`
# type: type
@dataclass
class Pet:
    id: ...
    category: ...
    name: ...
    photoUrls: ...
    tags: ...
    status: ...


# %% 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: Dataclass Relations Addressbook
# - Difficulty: easy
# - Lines: 8
# - Minutes: 3

# %% 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ść


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

>>> 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 'postcode' in address, \
'Class Address is missing field: postcode'
>>> 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], \
'User.addresses has invalid type annotation, expected: list[Address]'
>>> assert address['street'] == str, \
'Address.street has invalid type annotation, expected: str'
>>> assert address['city'] is str, \
'Address.city has invalid type annotation, expected: str'
>>> assert address['postcode'] is int, \
'Address.postcode 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",
         "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"}]}
]


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


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


# %% 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: Dataclass Relations DatabaseDump
# - Difficulty: medium
# - Lines: 12
# - Minutes: 8

# %% English
# 1. You received input data in JSON format from the API
#    - `str` fields: firstname, lastname, role, username, password, email,
#    - `date` field: birthdate,
#    - `datetime` field: last_login (optional),
#    - `bool` fields: is_active, is_staff, is_superuser,
#    - `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
#    - pola `str`: firstname, lastname, role, username, password, email,
#    - pole `date`: birthdate,
#    - pole `datetime`: last_login (opcjonalne),
#    - pola `bool`: is_active, is_staff, is_superuser,
#    - 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
>>> assert sys.version_info >= (3, 10), \
'Python 3.10+ required'

>>> 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', 'birthdate',
 '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'>,
 'birthdate': <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',
     birthdate='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","birthdate":"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=","birthdate":"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"'
    ',"birthdate":"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'
    'thdate":"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","birthdate":"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","birthdate":"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`
# - `str` fields: firstname, lastname, role, username, password, email,
# - `date` field: birthdate,
# - `datetime` field: last_login (optional),
# - `bool` fields: is_active, is_staff, is_superuser,
# - `list[dict]` field: user_permissions
# Leave `permission` attribute as `list[dict]`
# Note, that fields order is important for tests to pass
# type: type
@dataclass
class User:
    ...