8.5. Core Model

  • Models represents database entities (tables)

  • Metadata represents connection between Python object and a database

8.5.1. SetUp

Prepare connection:

>>> from sqlalchemy import create_engine
>>>
>>> engine = create_engine('sqlite:///:memory:', future=True)

Note, that we will use future flag to turn on the 2.0 compatibility mode.

8.5.2. Metadata

Metadata represents connection between Python object and a database. In order to create a Metadata object import it from sqlalchemy:

>>> from sqlalchemy import MetaData
>>>
>>> metadata = MetaData()

8.5.3. Database Model

>>> from sqlalchemy import  Table, Column
>>> from sqlalchemy import Integer, String, Date, Numeric, Enum
>>>
>>>
>>> astronaut = Table('astronaut', metadata,
...     Column('id', Integer, primary_key=True),
...     Column('firstname', String(50), nullable=False),
...     Column('lastname', String(50), nullable=False),
...     Column('birthdate', Date),
...     Column('height', Integer),
...     Column('weight', Numeric(3,2)),
...     Column('agency', Enum('NASA', 'ESA', 'POLSA')),
... )

8.5.4. Execute

Execute the statement to create database table:

>>> with engine.begin() as db:
...     astronaut.create(db)

SQLAlchemy core will generate and execute the following SQL statement. Note, that the CREATE TABLE statement is inside of the transaction. This will ensure database consistency and rollback if any problem occur (for example database table with the same name already exists):

BEGIN
CREATE TABLE astronaut (
    id INTEGER NOT NULL,
    firstname VARCHAR(50) NOT NULL,
    lastname VARCHAR(50) NOT NULL,
    birthdate DATE,
    height NUMERIC(3, 2),
    weight NUMERIC(3, 2),
    agency VARCHAR(9),
    PRIMARY KEY (id)
)
COMMIT

8.5.5. Recap

>>> from sqlalchemy import create_engine, MetaData, Table, Column
>>> from sqlalchemy import Integer, String, Date, Numeric, Enum, Float
>>>
>>>
>>> engine = create_engine('sqlite:///:memory:', future=True)
>>> metadata = MetaData()
>>>
>>> astronaut = Table('astronaut', metadata,
...     Column('id', Integer, primary_key=True),
...     Column('firstname', String(50), nullable=False),
...     Column('lastname', String(50), nullable=False),
...     Column('agency', Enum('NASA', 'ESA', 'POLSA')),
...     Column('birthdate', Date),
...     Column('age', Integer),
...     Column('height', Float(3,2)),
...     Column('weight', Float(3,2)),
... )
>>>
>>> with engine.begin() as db:
...     metadata.create_all(db)

8.5.6. Assignments

Code 8.43. Solution
"""
* Assignment: Model Data Iris
* Complexity: easy
* Lines of code: 8 lines
* Time: 8 min

English:
    1. Model data in `DATA`
        a. What is the name of the model?
        b. What are the names of the fields?
        c. What are the types of fields?
    2. Non-functional requirements:
        a. Use SQLAlchemy ORM to create models
        b. Do not convert data, just model it
        c. You can use any module from the standard library
        d. You can use SQLAlchemy and Alembic
        e. Do not install or use additional packages

Polish:
    1. Zamodeluj dane w `DATA`
        a. Jak nazywa się model?
        b. Jak nazywają się pola?
        c. Jakie typy mają pola?
    2. Wymagania niefunkcjonalne:
        a. Użyj SQLAlchemy ORM do stworzenia modeli
        b. Nie konwertuj danych, tylko je zamodeluj
        c. Możesz użyć dowolnego modułu z biblioteki standardowej
        d. Możesz użyć SQLAlchemy i Alembic
        e. Nie instaluj ani nie używaj dodatkowych pakietów

TODO: Automated tests
"""

DATA = [
    ('sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species'),
    (5.8, 2.7, 5.1, 1.9, 'virginica'),
    (5.1, 3.5, 1.4, 0.2, 'setosa'),
    (5.7, 2.8, 4.1, 1.3, 'versicolor'),
    (6.3, 2.9, 5.6, 1.8, 'virginica'),
    (6.4, 3.2, 4.5, 1.5, 'versicolor'),
    (4.7, 3.2, 1.3, 0.2, 'setosa'),
]

Code 8.44. Solution
"""
* Assignment: Model Data AstronautAgency
* Complexity: easy
* Lines of code: 11 lines
* Time: 8 min

English:
    1. Create propper classes to model the data:
       a. Watney, USA, 1969-07-21
       b. NASA, USA, 1958-07-29
    2. Create instances (watney, nasa) filling it with data
    3. Non-functional requirements:
        a. Use SQLAlchemy ORM to create models
        b. Do not convert data, just model it
        c. You can use any Python standard library module
        d. You can use SQLAlchemy and Alembic
        e. Do not install or use 3rd party modules
    4. Run doctests - all must succeed

Polish:
    1. Stwórz odpowiednie klasy aby zamodelować dane:
       a. Watney, USA, 1969-07-21
       b. NASA, USA, 1958-07-29
    2. Stwórz instancje (watney, nasa) wypełniając je danymi
    3. Wymagania niefunkcjonalne:
        a. Użyj SQLAlchemy ORM do stworzenia modeli
        b. Nie konwertuj danych, tylko je zamodeluj
        c. Możesz użyć dowolnego modułu z biblioteki standardowej
        d. Możesz użyć SQLAlchemy i Alembic
        e. Nie instaluj ani nie używaj dodatkowych pakietów
    4. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> assert 'Watney' in vars(watney).values()
    >>> assert 'USA' in vars(watney).values()
    >>> assert '1969-07-21' in vars(watney).values()
    >>> assert 'NASA' in vars(nasa).values()
    >>> assert 'USA' in vars(nasa).values()
    >>> assert '1958-07-29' in vars(nasa).values()
"""


# Model from data: 'Watney', 'USA', '1969-07-21'
# type: object
watney = ...

# Model from data: 'NASA', 'USA', '1958-07-29'
# type: object
nasa = ...


Code 8.45. Solution
"""
* Assignment: Model Data PetstoreFlat
* Complexity: easy
* Lines of code: 8 lines
* Time: 3 min

English:
    1. You received input data in JSON format from the API
    2. Using `dataclass` model data to create class `Pet`
    3. Non-functional requirements:
        a. Use SQLAlchemy ORM to create models
        b. Do not convert data, just model it
        c. You can use any Python standard library module
        d. You can use SQLAlchemy and Alembic
        e. Do not install or use 3rd party modules
    4. Run doctests - all must succeed

Polish:
    1. Otrzymałeś z API dane wejściowe w formacie JSON
    2. Wykorzystując `dataclass` zamodeluj dane aby stwórzyć klasę `Pet`
    3. Wymagania niefunkcjonalne:
        a. Użyj SQLAlchemy ORM do stworzenia modeli
        b. Nie konwertuj danych, tylko je zamodeluj
        c. Możesz użyć dowolnego modułu z biblioteki standardowej
        d. Możesz użyć SQLAlchemy i Alembic
        e. Nie instaluj ani nie używaj dodatkowych pakietów
    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 is_dataclass(Pet)

    >>> 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  # doctest: +NORMALIZE_WHITESPACE
    Pet(id=0, category='dogs', name='doggie', photoUrls='img/dogs/0.png',
        tags=['dog', 'hot-dog'], status='available')
"""

from dataclasses import dataclass

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

# Using `dataclass` model data to create class `Pet`
# type: type
class Pet:
    ...


Code 8.46. Solution
"""
* Assignment: Model Data PetstoreNested
* Complexity: easy
* Lines of code: 8 lines
* Time: 3 min

English:
    1. You received input data in JSON format from the API
    2. Using `dataclass` model `DATA` to create class `Pet`
       a. Leave `category` as `dict`
       b. Leave `tags` as `list[dicts]`
    3. Non-functional requirements:
        a. Use SQLAlchemy ORM to create models
        b. Do not convert data, just model it
        c. You can use any Python standard library module
        d. You can use SQLAlchemy and Alembic
        e. Do not install or use 3rd party modules
    4. Run doctests - all must succeed

Polish:
    1. Otrzymałeś z API dane wejściowe w formacie JSON
    2. Wykorzystując `dataclass` zamodeluj `DATA` aby stwórzyć klasę `Pet`
       a. Pozostaw `category` jako `dict`
       b. Pozostaw `tags` jako `list[dicts]`
    3. Wymagania niefunkcjonalne:
        a. Użyj SQLAlchemy ORM do stworzenia modeli
        b. Nie konwertuj danych, tylko je zamodeluj
        c. Możesz użyć dowolnego modułu z biblioteki standardowej
        d. Możesz użyć SQLAlchemy i Alembic
        e. Nie instaluj ani nie używaj dodatkowych pakietów
    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 is_dataclass(Pet)

    >>> 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  # doctest: +NORMALIZE_WHITESPACE
    Pet(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')
"""

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 to create class `Pet`
# type: type
class Pet:
    ...

Code 8.47. Solution
"""
* Assignment: Model Data PetstoreRelations
* 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. Non-functional requirements:
        a. Use SQLAlchemy ORM to create models
        b. Do not convert data, just model it
        c. You can use any Python standard library module
        d. You can use SQLAlchemy and Alembic
        e. Do not install or use 3rd party modules
    5. 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. Wymagania niefunkcjonalne:
        a. Użyj SQLAlchemy ORM do stworzenia modeli
        b. Nie konwertuj danych, tylko je zamodeluj
        c. Możesz użyć dowolnego modułu z biblioteki standardowej
        d. Możesz użyć SQLAlchemy i Alembic
        e. Nie instaluj ani nie używaj dodatkowych pakietów
    5. 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
class Category:
    ...

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

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


Code 8.48. Solution
"""
* Assignment: Model Data 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:
       a. Do not use Python 3.10 syntax for Optionals, ie: `str | None`
       b. Use old style `Optional[str]` instead
    3. Non-functional requirements:
        a. Use SQLAlchemy ORM to create models
        b. Do not convert data, just model it
        c. You can use any Python standard library module
        d. You can use SQLAlchemy and Alembic
        e. Do not install or use 3rd party modules
    4. Run doctests - all must succeed

Polish:
    1. Zamodeluj `DATA` wykorzystując `dataclass`
    2. Stwórz definicję klas, pól i ich typów
       a. Nie używaj składni Optionali z Python 3.10, np.: `str | None`
       b. Użyj starego sposobu, tj. `Optional[str]`
    3. Wymagania niefunkcjonalne:
        a. Użyj SQLAlchemy ORM do stworzenia modeli
        b. Nie konwertuj danych, tylko je zamodeluj
        c. Możesz użyć dowolnego modułu z biblioteki standardowej
        d. Możesz użyć SQLAlchemy i Alembic
        e. Nie instaluj ani nie używaj dodatkowych pakietów
    4. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(Astronaut)
    >>> assert isclass(Address)
    >>> assert is_dataclass(Astronaut)
    >>> assert is_dataclass(Address)

    >>> astronaut = Astronaut.__dataclass_fields__
    >>> address = Address.__dataclass_fields__

    >>> assert 'firstname' in astronaut, \
    'Class Astronaut is missing field: firstname'
    >>> assert 'lastname' in astronaut, \
    'Class Astronaut is missing field: lastname'
    >>> assert 'addresses' in astronaut, \
    'Class Astronaut 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 astronaut['firstname'].type is str, \
    'Astronaut.firstname has invalid type annotation, expected: str'
    >>> assert astronaut['lastname'].type is str, \
    'Astronaut.lastname has invalid type annotation, expected: str'
    >>> assert astronaut['addresses'].type.__name__ == 'list', \
    'Astronaut.addresses has invalid type annotation, expected: list[Address]'
    >>> assert address['street'].type is Optional[str], \
    'Address.street has invalid type annotation, expected: Optional[str]'
    >>> assert address['city'].type is str, \
    'Address.city has invalid type annotation, expected: str'
    >>> assert address['post_code'].type is Optional[int], \
    'Address.post_code has invalid type annotation, expected: Optional[int]'
    >>> assert address['region'].type is str, \
    'Address.region has invalid type annotation, expected: str'
    >>> assert address['country'].type is str, \
    'Address.country has invalid type annotation, expected: str'

TODO: Add support for Python 3.10 Optional and Union syntax
"""
from dataclasses import dataclass, field
from typing import Optional


DATA = [
    {"firstname": "José", "lastname": "Jiménez", "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": "Mark", "lastname": "Watney", "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": "Иван", "lastname": "Иванович", "addresses": [
        {"street": None, "city": "Космодро́м Байкону́р", "post_code": None,
         "region": "Кызылординская область", "country": "Қазақстан"},
        {"street": None, "city": "Звёздный городо́к", "post_code": 141160,
         "region": "Московская область", "country": "Россия"}]},

    {"firstname": "Melissa", "lastname": "Lewis"},

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


# Model `DATA` using `dataclasses`, do not use: `str | None` syntax
# type: type
class Address:
    ...

# Model `DATA` using `dataclasses`, do not use: `str | None` syntax
# type: type
class Astronaut:
    ...


Code 8.49. Solution
# TODO: Poprawić formatowanie dbdump
"""
* Assignment: Model Data 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. `datetime` fields: birthdate, last_login,
       c. `bool` fields: is_active, is_staff, is_superuser,
       d. `list[dict]` field: user_permissions
    2. Using `dataclass` model data as class `User`
       a. Note, that fields order is important for tests to pass
    3. Do not create additional classes to represent `permission` filed,
       leave it as `list[dict]`
    4. Non-functional requirements:
        a. Use SQLAlchemy ORM to create models
        b. Do not convert data, just model it
        c. You can use any Python standard library module
        d. You can use SQLAlchemy and Alembic
        e. Do not install or use 3rd party modules
    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. pola `datetime`: birthdate, last_login,
       c. pola `bool`: is_active, is_staff, is_superuser,
       d. pola `list[dict]`: user_permissions
    2. Wykorzystując `dataclass` zamodeluj dane za pomocą klasy `User`
       a. Zwróć uwagę, że kolejność pól ma znaczenie aby testy przechodziły
    3. Nie twórz dodatkowych klas do reprezentacji pola `permission`,
       niech zostanie jako `list[dict]`
    4. Wymagania niefunkcjonalne:
        a. Użyj SQLAlchemy ORM do stworzenia modeli
        b. Nie konwertuj danych, tylko je zamodeluj
        c. Możesz użyć dowolnego modułu z biblioteki standardowej
        d. Możesz użyć SQLAlchemy i Alembic
        e. Nie instaluj ani nie używaj dodatkowych pakietów
    5. Uruchom doctesty - wszystkie muszą się powieść

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',
     'birthdate', 'last_login', 'is_active', 'is_staff', 'is_superuser',
     'user_permissions']

    >>> 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"]}]}}]')

# Using `dataclass` model data as class `User`
# type: type
class User:
    ...


Code 8.50. Solution
"""
* Assignment: Model Data AddresBook
* Complexity: easy
* Lines of code: 10 lines
* Time: 8 min

English:
    1. In `DATA` we have two classes
    2. Model data using classes and relations
    3. Create instances dynamically based on `DATA`
    4. Non-functional requirements:
        a. Use SQLAlchemy ORM to create models
        b. Do not convert data, just model it
        c. You can use any Python standard library module
        d. You can use SQLAlchemy and Alembic
        e. Do not install or use 3rd party modules
    5. 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. Wymagania niefunkcjonalne:
        a. Użyj SQLAlchemy ORM do stworzenia modeli
        b. Nie konwertuj danych, tylko je zamodeluj
        c. Możesz użyć dowolnego modułu z biblioteki standardowej
        d. Możesz użyć SQLAlchemy i Alembic
        e. Nie instaluj ani nie używaj dodatkowych pakietów
    5. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> assert type(result) is list

    >>> assert all(type(user) is Astronaut
    ...            for user in result)

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

    >>> result  # doctest: +NORMALIZE_WHITESPACE
    [Astronaut(firstname='José',
               lastname='Jiménez',
               addresses=[Address(street='2101 E NASA Pkwy', city='Houston', postcode=77058, region='Texas', country='USA'),
                          Address(street=None, city='Kennedy Space Center', postcode=32899, region='Florida', country='USA')]),
     Astronaut(firstname='Mark',
               lastname='Watney',
               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')]),
     Astronaut(firstname='Иван',
               lastname='Иванович',
               addresses=[Address(street=None, city='Космодро́м Байкону́р', postcode=None, region='Кызылординская область', country='Қазақстан'),
                          Address(street=None, city='Звёздный городо́к', postcode=141160, region='Московская область', country='Россия')]),
     Astronaut(firstname='Melissa',
               lastname='Lewis',
               addresses=[]),
     Astronaut(firstname='Alex',
               lastname='Vogel',
               addresses=[Address(street='Linder Hoehe', city='Cologne', postcode=51147, region='North Rhine-Westphalia', country='Germany')])]
"""

from dataclasses import dataclass


DATA = [
    {"firstname": "José", "lastname": "Jiménez", "addresses": [
        {"street": "2101 E NASA Pkwy", "city": "Houston", "postcode": 77058, "region": "Texas", "country": "USA"},
        {"street": None, "city": "Kennedy Space Center", "postcode": 32899, "region": "Florida", "country": "USA"}]},
    {"firstname": "Mark", "lastname": "Watney", "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": "Иван", "lastname": "Иванович", "addresses": [
        {"street": None, "city": "Космодро́м Байкону́р", "postcode": None, "region": "Кызылординская область", "country": "Қазақстан"},
        {"street": None, "city": "Звёздный городо́к", "postcode": 141160, "region": "Московская область", "country": "Россия"}]},
    {"firstname": "Melissa", "lastname": "Lewis", "addresses": []},
    {"firstname": "Alex", "lastname": "Vogel", "addresses": [
        {"street": "Linder Hoehe", "city": "Cologne", "postcode": 51147, "region": "North Rhine-Westphalia", "country": "Germany"}]}
]

class Astronaut:
    ...

class Address:
    ...


# Iterate over `DATA` and create instances
# type: list[Astronaut]
result = ...