Была как-то задача по написанию бота для корпоративного чата, самого бота к сожалению рассмотреть здесь я не могу, но могу выложить интересный пример по генерации json на основе полей класса в python.
Собственно одна из функций бота это создацие индидентов в системе инцидент менеджмента Pager Duty, а также заведение тикетов в YouTrack. В обоих случаях нам надо сгенирировать json для его загрузки в API.
Вот так должне выглядить финальный json для создание инцидента в PD:
{
"incident": {
"type": "incident",
"title": "<override>",
"service": {
"id": "<override>",
"type": "service_reference"
},
"body": {
"type": "incident_body",
"details": "<override>"
},
"urgency": "<override>"
}
}
Везде, где есть <override> должно подставится наше кастомное значение.
Вот таким должен выглядеть json для YouTrack:
{
"project": {
"id": "<ID проекта в YT>"
},
"summary": "<Заголовок задачи>",
"description": "<Описание задачи>",
"customFields": [
{
"name": "Assignee",
"type": "SingleUserIssueCustomField",
"value": {
"login": "<Логин пользователя>"
}
},
{
"name": "State",
"type": "StateIssueCustomField",
"value": {
"name": "<Состояние задачи>"
}
}
]
}
Конечно оба варианта могут быть значительно больше по количеству элементов, но тут мы рассматриваем не все возможные возможности API того или иного продукта, а как нам на основе полей конструктора сгенерировать json.
Создаем класс для нашей задачи:
from dataclasses import dataclass
import json
@dataclass
class YouTrackBaseFields:
project: dict
summary: str
description: str
customFields: list
def toJson(self):
return json.dumps(self, default=lambda o: o.__dict__, indent=4)
@dataclass
class YouTrackProjectFields:
id: str
@dataclass
class YouTrackCustomFields:
name: str
type: str
value: dict
@dataclass
class YouTrackCustomFieldsValueGen:
def gen_fields(self, **kwargs):
data = dict
if "login" in kwargs:
data = {"login":kwargs["login"]}
elif "name" in kwargs:
data = {"name":kwargs["name"]}
elif "text" in kwargs:
data = {"text":kwargs["text"]}
return data
@dataclass
class PagerDutyBaseFields:
incident: dict
def toJson(self):
return json.dumps(self, default=lambda o: o.__dict__, indent=4)
@dataclass
class PagerDutyIncidentFields:
type: str
title: str
service: dict
body: dict
urgency: str
@dataclass
class PagerDutyBodyFields:
type: str
details: str
@dataclass
class PagerDutyServiceFields:
id: str
type: str
Разбираем классы Ютрека:
YouTrackBaseFields:
class YouTrackBaseFields:
project: dict
summary: str
description: str
customFields: list
Тут мы описываем базовые поля класса, от которых будет по сути идти ответвление дальше. Таким образом это «скелет» для нашего будещего json. Имена полей важны, так как они будут совпадать с тем, что «просочится» в наш json.
В этом же классе мы реализовываем метод toJson, который вернет json.
YouTrackProjectFields:
class YouTrackProjectFields:
id: str
имеет одно поле id, которое является словарем.
YouTrackCustomFields:
class YouTrackCustomFields:
name: str
type: str
value: dict
Тут описываем элементы кастомных полей.
YouTrackCustomFieldsValueGen:
class YouTrackCustomFieldsValueGen:
def gen_fields(self, **kwargs):
data = dict
if "login" in kwargs:
data = {"login":kwargs["login"]}
elif "name" in kwargs:
data = {"name":kwargs["name"]}
elif "text" in kwargs:
data = {"text":kwargs["text"]}
return data
У Ютрека довольно развесистая структура json, внутри которой есть такие структуры данных как списки, словари и прочее. Код выше упрощает работу с этим и генерирует нам то, как будет выглядеть поле, в зависимости от переданного значения.
Разбираем классы Pager Duty:
PagerDutyBaseFields:
class PagerDutyBaseFields:
incident: dict
В Pager Duty по проще, тут все входит в ключ incident.
PagerDutyIncidentFields:
class PagerDutyIncidentFields:
type: str
title: str
service: dict
body: dict
urgency: str
Описываем каркас полей для ключа incident.
PagerDutyBodyFields:
class PagerDutyBodyFields:
type: str
details: str
Так как body является словарем, внутри которого два элемента, описываем их отдельным классом.
PagerDutyServiceFields:
class PagerDutyServiceFields:
id: str
type: str
Тут аналогично классу PagerDutyBodyFields.
Собираем все вместе:
На данном этапе мы описали структуру классов, которая соответствует структуре json. Осталось это правильно использовать.
Давайте создадим main.py и импортируем в него нужные нам классы:
from json_ser import (
PagerDutyBaseFields,
PagerDutyBodyFields,
PagerDutyIncidentFields,
PagerDutyServiceFields,
YouTrackBaseFields,
YouTrackCustomFields,
YouTrackCustomFieldsValueGen,
YouTrackProjectFields
)
Далее описываем функцию, которая будет генерировать json для Pager Duty:
def pd_json_template():
body_fields = PagerDutyBodyFields("incident_body", "some details")
service_fields = PagerDutyServiceFields("1234", "service_reference")
incident_fields = PagerDutyIncidentFields("incident", "Some title",
service_fields, body_fields, "some urgency")
main_fields = PagerDutyBaseFields(incident_fields)
payload = main_fields.toJson()
return payload
Переменную body_fields мы делаем экземпляром класса, описывающего структуру поля body. У этого поля есть поля конструктора type и details, именно их мы и передаем в качестве аргументов.
Похожую операцию производим для service_fields, а вот в экземпляр класс incident_fields мы помимо прочего передаем экземпляры класса, созданные выше. И уже в main_fields передаем наш экземпляр incident_fields, который в себе включает все то, что мы описали ранее. Таким образ мы собираем по частям наш json, упаковывая его как бы слоями, разбив перед этим на отдельные «запчасти» и собрав их вместе в конце.
Давайте запустим код и посмотрим вывод:

Отлично, это именно то, что было нужно. Теперь это можно сохранить в переменную и отправить в качестве данных для загрузки в API Pager Duty.
Метод для Ютрека:
def yt_json_tempalte():
project_field = YouTrackProjectFields("1234")
custom_field_value_generator = YouTrackCustomFieldsValueGen()
assaigne_field = YouTrackCustomFields("Assignee", "SingleUserIssueCustomField",
custom_field_value_generator.gen_fields(login="username"))
state_field = YouTrackCustomFields("State", "StateIssueCustomField",
custom_field_value_generator.gen_fields(name="В очереди"))
base_field = YouTrackBaseFields(project_field, "YT summary",
"YT description", [assaigne_field, state_field])
data = base_field.toJson()
return data
Здесь ситуация аналогичная, мы собираем «по запчастям» json. Если посмотреть на то, как описаны классы и на то, как создаются экземпляры классов и какие параметры в них передаются, все станет понятно.
Запускаем код:

Видим символы, вместо слова «В очереди», которое мы указывали. Дело в том, что по умолчанию при работе с json в python установлен такой параметр — ensure_ascii=True. Это значит, что все символы, которые не являются ASCII, будут экранированы, чтобы гарантировать, что результат будет содержать только ASCII-символы. Это можно оставить как есть, ведь если мы планируем отправлять данные по сети, то так и должно быть, ошибки при загрузке такого json в API не будет. Но если мы хотим обычным print напечатать корректно, то достаточно переопределить этот параметр в методе toJson():
def toJson(self):
return json.dumps(self, default=lambda o: o.__dict__, indent=4, ensure_ascii=False)
Выводим json снова:

Все отображается корректно.
Это базовый пример, который показывает как можно использовать классы и json вместе. Это удобно, так как в будущем, чтобы добавить поддержку нового поля например в customFields для Ютрека, достаточно будет просто создать новый экземпляр класса.
Полный файл main.py:
from json_ser import (
PagerDutyBaseFields,
PagerDutyBodyFields,
PagerDutyIncidentFields,
PagerDutyServiceFields,
YouTrackBaseFields,
YouTrackCustomFields,
YouTrackCustomFieldsValueGen,
YouTrackProjectFields
)
def pd_json_template():
body_fields = PagerDutyBodyFields("incident_body", "some details")
service_fields = PagerDutyServiceFields("1234", "service_reference")
incident_fields = PagerDutyIncidentFields("incident", "Some title",
service_fields, body_fields, "some urgency")
main_fields = PagerDutyBaseFields(incident_fields)
payload = main_fields.toJson()
return payload
def yt_json_tempalte():
project_field = YouTrackProjectFields("1234")
custom_field_value_generator = YouTrackCustomFieldsValueGen()
assaigne_field = YouTrackCustomFields("Assignee", "SingleUserIssueCustomField",
custom_field_value_generator.gen_fields(login="username"))
state_field = YouTrackCustomFields("State", "StateIssueCustomField",
custom_field_value_generator.gen_fields(name="В очереди"))
base_field = YouTrackBaseFields(project_field, "YT summary",
"YT description", [assaigne_field, state_field])
data = base_field.toJson()
return data
def main():
print(pd_json_template())
print(yt_json_tempalte())
if __name__ == "__main__":
main()