7.6. JSON Object

7.6.1. SetUp

>>> from pprint import pprint
>>> from dataclasses import dataclass
>>> import json

7.6.2. Encode Object

>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
>>>
>>>
>>> mark = Astronaut('Mark', 'Watney')
>>> data = vars(mark)
>>>
>>> json.dumps(data)
'{"firstname": "Mark", "lastname": "Watney"}'

7.6.3. Decode Object

>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
>>>
>>>
>>> DATA = """{
...   "firstname": "Mark",
...   "lastname": "Watney"
... }"""
>>>
>>> data = json.loads(DATA)
>>> result = Astronaut(**data)
>>>
>>> print(result)
Astronaut(firstname='Mark', lastname='Watney')

7.6.4. Object Encoder

>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
>>>
>>>
>>> data = Astronaut('Mark', 'Watney')
>>>
>>>
>>> def encoder(obj):
...     return vars(obj)
>>>
>>>
>>> json.dumps(data, default=encoder)
'{"firstname": "Mark", "lastname": "Watney"}'

7.6.5. Object Decoder

>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
>>>
>>>
>>> DATA = """{
...   "firstname": "Mark",
...   "lastname": "Watney"
... }"""
>>>
>>>
>>> def decoder(data):
...     return Astronaut(**data)
>>>
>>>
>>> json.loads(DATA, object_hook=decoder)
Astronaut(firstname='Mark', lastname='Watney')

7.6.6. Encode Object with Relation

>>> @dataclass
... class Mission:
...     year: int
...     name: str
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     role: str
...     missions: list[Mission]
>>>
>>>
>>> CREW = [
...     Astronaut('Mark', 'Watney', 'Botanist', missions=[
...         Mission(2035, 'Ares 3')]),
...     Astronaut('Melissa', 'Lewis', 'Commander', missions=[
...         Mission(2035, 'Ares 3'),
...         Mission(2031, 'Ares 1')]),
...     Astronaut('Rick', 'Martinez', 'Pilot', missions=[])]
>>>
>>>
>>> def encoder(obj):
...     data = {'_type': obj.__class__.__name__}
...     return data | vars(obj)
>>>
>>>
>>> result = json.dumps(CREW, default=encoder, indent=2)
>>>
>>> print(result)
[
  {
    "_type": "Astronaut",
    "firstname": "Mark",
    "lastname": "Watney",
    "role": "Botanist",
    "missions": [
      {
        "_type": "Mission",
        "year": 2035,
        "name": "Ares 3"
      }
    ]
  },
  {
    "_type": "Astronaut",
    "firstname": "Melissa",
    "lastname": "Lewis",
    "role": "Commander",
    "missions": [
      {
        "_type": "Mission",
        "year": 2035,
        "name": "Ares 3"
      },
      {
        "_type": "Mission",
        "year": 2031,
        "name": "Ares 1"
      }
    ]
  },
  {
    "_type": "Astronaut",
    "firstname": "Rick",
    "lastname": "Martinez",
    "role": "Pilot",
    "missions": []
  }
]

7.6.7. Decode

Encoding nested objects with relations to JSON:

>>> @dataclass
... class Mission:
...     year: int
...     name: str
>>>
>>>
>>> @dataclass
... class Astronaut:
...     firstname: str
...     lastname: str
...     role: str
...     missions: list[Mission]
>>>
>>>
>>> DATA = """[{"_type": "Astronaut", "firstname": "Mark", "lastname": "Watney", "role": "Botanist", "missions": [{"_type": "Mission", "year": 2035, "name": "Ares 3"}]}, {"_type": "Astronaut", "firstname": "Melissa", "lastname": "Lewis", "role": "Commander", "missions": [{"_type": "Mission", "year": 2035, "name": "Ares 3"}, {"_type": "Mission", "year": 2031, "name": "Ares 1"}]}, {"_type": "Astronaut", "firstname": "Rick", "lastname": "Martinez", "role": "Pilot", "missions": []}]"""
>>>
>>>
>>> def decoder(obj):
...     clsname = obj.pop('_type')
...     cls = globals()[clsname]
...     return cls(**obj)
>>>
>>>
>>> result = json.loads(DATA, object_hook=decoder)
>>>
>>> pprint(result, width=72)
[Astronaut(firstname='Mark',
           lastname='Watney',
           role='Botanist',
           missions=[Mission(year=2035, name='Ares 3')]),
 Astronaut(firstname='Melissa',
           lastname='Lewis',
           role='Commander',
           missions=[Mission(year=2035, name='Ares 3'),
                     Mission(year=2031, name='Ares 1')]),
 Astronaut(firstname='Rick',
           lastname='Martinez',
           role='Pilot',
           missions=[])]

7.6.8. Use Case - 0x01

>>> import json
>>> from dataclasses import dataclass, field
>>> from pprint import pprint
>>>
>>>
>>> @dataclass
... class Mission:
...     year: int
...     name: str
>>>
>>>
>>> @dataclass
... class Astronaut:
...     lastname: str
...     firstname: str
...     missions: list[Mission] = field(default_factory=list)
>>>
>>>
>>> CREW = [
...     Astronaut('Mark', 'Watney', missions=[
...         Mission(1973, 'Apollo18'),
...         Mission(2035, 'Ares3'),
...     ]),
...
...     Astronaut('Melissa', 'Lewis', missions=[
...         Mission(2035, 'Ares3'),
...     ]),
...
...     Astronaut('Rick', 'Martinez'),
... ]
>>>
>>>
>>> class MyEncoder(json.JSONEncoder):
...     def default(self, obj):
...         data = vars(obj)
...         data['__clsname__'] = obj.__class__.__name__
...         return data
>>>
>>>
>>> class MyDecoder(json.JSONDecoder):
...     def __init__(self):
...         super().__init__(object_hook=self.default)
...
...     def default(self, data: dict) -> dict:
...         clsname = data.pop('__clsname__')
...         cls = globals()[clsname]
...         return cls(**data)
>>> result = json.dumps(CREW, cls=MyEncoder)
>>>
>>> pprint(result, width=72)
('[{"lastname": "Mark", "firstname": "Watney", "missions": [{"year": '
 '1973, "name": "Apollo18", "__clsname__": "Mission"}, {"year": 2035, '
 '"name": "Ares3", "__clsname__": "Mission"}], "__clsname__": '
 '"Astronaut"}, {"lastname": "Melissa", "firstname": "Lewis", '
 '"missions": [{"year": 2035, "name": "Ares3", "__clsname__": '
 '"Mission"}], "__clsname__": "Astronaut"}, {"lastname": "Rick", '
 '"firstname": "Martinez", "missions": [], "__clsname__": '
 '"Astronaut"}]')
>>> result = json.loads(result, cls=MyDecoder)
>>>
>>> pprint(result)  
[Astronaut(lastname='Mark',
           firstname='Watney',
           missions=[Mission(year=1973, name='Apollo18'),
                     Mission(year=2035, name='Ares3')]),
 Astronaut(lastname='Melissa',
           firstname='Lewis',
           missions=[Mission(year=2035, name='Ares3')]),
 Astronaut(lastname='Rick', firstname='Martinez', missions=[])]

7.6.9. Assignments

Code 7.17. Solution
"""
* Assignment: JSON Object Factory
* Complexity: medium
* Lines of code: 5 lines
* Time: 5 min

English:
    1. Convert from JSON format to Python using decoder function
    2. Create instances of `Setosa`, `Virginica`, `Versicolor`
       classes based on value in field "species"
    3. Add instances to `result: list[Setosa|Virginica|Versicolor]`
    4. Run doctests - all must succeed

Polish:
    1. Przekonwertuj dane z JSON do Python używając dekodera funkcyjnego
    2. Twórz obiekty klas `Setosa`, `Virginica`, `Versicolor`
       w zależności od wartości pola "species"
    3. Dodawaj instancje do `result: list[Setosa|Virginica|Versicolor]`
    4. Uruchom doctesty - wszystkie muszą się powieść

Hint:
    * `dict.pop()`
    * `globals()`
    * Assignment Expression

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

    >>> assert type(result)
    >>> assert len(result) == 9

    >>> classes = (Setosa, Virginica, Versicolor)
    >>> assert all(type(row) in classes for row in result)

    >>> result[0]
    Virginica(sepalLength=5.8, sepalWidth=2.7, petalLength=5.1, petalWidth=1.9)

    >>> result[1]
    Setosa(sepalLength=5.1, sepalWidth=3.5, petalLength=1.4, petalWidth=0.2)
"""

import json
from dataclasses import dataclass

FILE = r'_temporary.json'

DATA = (
    '[{"sepalLength":5.8,"sepalWidth":2.7,"petalLength":5.1,"petalWidth":1.9,'
    '"species":"virginica"},{"sepalLength":5.1,"sepalWidth":3.5,"petalLength"'
    ':1.4,"petalWidth":0.2,"species":"setosa"},{"sepalLength":5.7,"sepalWidth'
    '":2.8,"petalLength":4.1,"petalWidth":1.3,"species":"versicolor"},{"sepal'
    'Length":6.3,"sepalWidth":2.9,"petalLength":5.6,"petalWidth":1.8,"species'
    '":"virginica"},{"sepalLength":6.4,"sepalWidth":3.2,"petalLength":4.5,"pe'
    'talWidth":1.5,"species":"versicolor"},{"sepalLength":4.7,"sepalWidth":3.'
    '2,"petalLength":1.3,"petalWidth":0.2,"species":"setosa"},{"sepalLength":'
    '7.0,"sepalWidth":3.2,"petalLength":4.7,"petalWidth":1.4,"species":"versi'
    'color"},{"sepalLength":7.6,"sepalWidth":3.0,"petalLength":6.6,"petalWidt'
    'h":2.1,"species":"virginica"},{"sepalLength":4.9,"sepalWidth":3.0,"petal'
    'Length":1.4,"petalWidth":0.2,"species":"setosa"}]')


@dataclass
class Iris:
    sepalLength: float
    sepalWidth: float
    petalLength: float
    petalWidth: float


class Setosa(Iris):
    pass


class Virginica(Iris):
    pass


class Versicolor(Iris):
    pass



# JSON decoded DATA
result = ...


Code 7.18. Solution
"""
* Assignment: JSON Object Dataclass
* Complexity: easy
* Lines of code: 15 lines
* Time: 13 min

English:
    1. `DATA` is a JSON downloaded from https://api.github.com/users
    3. Model data as class `User`
    4. Iterate over records and create instances of this class
    5. Collect all instances to one list
    6. Run doctests - all must succeed

Polish:
    1. `DATA` to JSON pobrany z https://api.github.com/users
    3. Zamodeluj dane za pomocą klasy `User`
    4. Iterując po rekordach twórz instancje tej klasy
    5. Zbierz wszystkie instancje do jednej listy
    6. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> type(result)
    <class 'list'>
    >>> len(result) > 0
    True
    >>> all(type(row) is User
    ...     for row in result)
    True
    >>> result[0]  # doctest: +NORMALIZE_WHITESPACE
    User(login='myuser',
         id=1,
         node_id='MDQ6VXNlcjE=',
         avatar_url='https://avatars.githubusercontent.com/u/1?v=4',
         gravatar_id='',
         url='https://api.github.com/users/myuser',
         html_url='https://github.com/myuser',
         followers_url='https://api.github.com/users/myuser/followers',
         following_url='https://api.github.com/users/myuser/following',
         gists_url='https://api.github.com/users/myuser/gists{/gist_id}',
         starred_url='https://api.github.com/users/myuser/starred',
         subscriptions_url='https://api.github.com/users/myuser/subscriptions',
         organizations_url='https://api.github.com/users/myuser/orgs',
         repos_url='https://api.github.com/users/myuser/repos',
         events_url='https://api.github.com/users/myuser/events{/privacy}',
         received_events_url='https://api.github.com/users/myuser',
         type='User',
         site_admin=False)
"""
import json


DATA = (
    '[{"login":"myuser","id":1,"node_id":"MDQ6VXNlcjE=","avatar_url":"http'
    's://avatars.githubusercontent.com/u/1?v=4","gravatar_id":"","url":"ht'
    'tps://api.github.com/users/myuser","html_url":"https://github.com/myu'
    'ser","followers_url":"https://api.github.com/users/myuser/followers",'
    '"following_url":"https://api.github.com/users/myuser/following","gist'
    's_url":"https://api.github.com/users/myuser/gists{/gist_id}","starred'
    '_url":"https://api.github.com/users/myuser/starred","subscriptions_ur'
    'l":"https://api.github.com/users/myuser/subscriptions","organizations'
    '_url":"https://api.github.com/users/myuser/orgs","repos_url":"https:/'
    '/api.github.com/users/myuser/repos","events_url":"https://api.github.'
    'com/users/myuser/events{/privacy}","received_events_url":"https://api'
    '.github.com/users/myuser","type":"User","site_admin":false},{"login":'
    '"defunkt","id":2,"node_id":"MDQ6VXNlcjI=","avatar_url":"https://avata'
    'rs.githubusercontent.com/u/2?v=4","gravatar_id":"","url":"https://api'
    '.github.com/users/defunkt","html_url":"https://github.com/defunkt","f'
    'ollowers_url":"https://api.github.com/users/defunkt/followers","follo'
    'wing_url":"https://api.github.com/users/defunkt/following","gists_url'
    '":"https://api.github.com/users/defunkt/gists{/gist_id}","starred_url'
    '":"https://api.github.com/users/defunkt/starred","subscriptions_url":'
    '"https://api.github.com/users/defunkt/subscriptions","organizations_u'
    'rl":"https://api.github.com/users/defunkt/orgs","repos_url":"https://'
    'api.github.com/users/defunkt/repos","events_url":"https://api.github.'
    'com/users/defunkt/events{/privacy}","received_events_url":"https://ap'
    'i.github.com/users/defunkt","type":"User","site_admin":false},{"login'
    '":"pjhyett","id":3,"node_id":"MDQ6VXNlcjM=","avatar_url":"https://ava'
    'tars.githubusercontent.com/u/3?v=4","gravatar_id":"","url":"https://a'
    'pi.github.com/users/pjhyett","html_url":"https://github.com/pjhyett",'
    '"followers_url":"https://api.github.com/users/pjhyett/followers","fol'
    'lowing_url":"https://api.github.com/users/pjhyett/following","gists_u'
    'rl":"https://api.github.com/users/pjhyett/gists{/gist_id}","starred_u'
    'rl":"https://api.github.com/users/pjhyett/starred","subscriptions_url'
    '":"https://api.github.com/users/pjhyett/subscriptions","organizations'
    '_url":"https://api.github.com/users/pjhyett/orgs","repos_url":"https:'
    '//api.github.com/users/pjhyett/repos","events_url":"https://api.githu'
    'b.com/users/pjhyett/events{/privacy}","received_events_url":"https://'
    'api.github.com/users/pjhyett","type":"User","site_admin":false}]')


class User:
    def __repr__(self):
        result = []
        for attrname, attrvalue in vars(self).items():
            if type(attrvalue) is str:
                result.append(f"{attrname}='{attrvalue}'")
            else:
                result.append(f"{attrname}={attrvalue}")
        result = ', '.join(result)
        return f"User({result})"


# JSON decoded DATA
result = ...