Стек: python
, Yandex.ClickHouse
, swagger\OpenApi
, JavaScript\bootstrap
Вы включены в команду разработки дополнительного программного обеспечения для проведения мониторинга всех этапов строительства на промышленной площадке. В настоящее время, у Вас в организации есть ERP система полного цикла которая позволяет вести учет всех данных связанных со строительством любых объектов. Однако, для проведения каждого этапа аудита требуется выезд на объект группы специалистов для оценки прогресса. Далее, составляется акт на бумажном носителе, Данный акт и набор документов рассылается всем субподрядчикам. Каждый субподрядчик, проверяет документы и ведет диалог с контроллирующим органом. Для ускорения данного процесса и повышения
прозрачности
деятельности принято решение расширить текущую ERP систему путем создание внешнего модуля (Rest Api) и отдельного WEB приложения (личный кабинет субподрядчика). Так же, ввиду того, что требуется не только хранить всю историю работы но и проводить анализ данных на предмет качества работы каждого субподрядчика, принято решение, включить во внешний модуль часть аналитики на основе облачной платформы Yandex.ClickHouse
- Установить ClickHouse - https://clickhouse.com/docs/en/install
- Установить Visual Studio Code - https://code.visualstudio.com/download
- Установить плагины: Python, PyTest (
ms-python.python
,littlefoxteam.vscode-python-test-adapter
,pamaron.pytest-runner
)
- Установить плагины: Python, PyTest (
- Установить pip утилиту
sudo apt install python3-pip
- Установить расширение для Python
pip install clickhouse-connect
- Установить драйвер для работы
pip install clickhouse-driver
- Установить расширение для Python
Основными сущностями в проекте будут считаться: объект капитального строительства
, застройщик
, субподрядчик
, исполнитель
Приложение должно оперировать следующими документами: акт
, реектр актов
В рамках каждого документа назначается оценка состояния: сумма штрафа
, статус работ
Определим статусы объекта:
Статус | Описание |
---|---|
Подготовка | Все документы для начала строительства / этапа собраны и проверены. Разрешения выданы |
Старт | На объекте начаты строительные работы. Представители субподрядчика прибыли на объект |
Проверка пройдена | Очередная проверка пройдена без замечаний со стороны контроллирующих органов |
Замечания | Очередная проверка пройдена с замечаниями. Необходимо устранить все замечания |
Завершено | Все замечания устранены. Все проверки пройдены. Оплату можно проводить |
Выбираем вариант - многослойная архитектура
В проекте будем разделять модели на: основные
, для передачи и обработки данных
Создадим пустой проект и добавим каталоги: Src/Models
, Tests
Создадим основные модели.
Модель | Наименование |
---|---|
progress_status | Описание статусов |
building | Объект капитального строительства |
contractor | Застройщик / Подрядчик / Субподрядчик |
executor | Исполнитель |
act | Документ Акт проверки |
period | Обвертка для работы с датой-временем |
guid | Обвертка для работы с уникальным кодом |
Так же, добавим модульные тесты.
Подготовка Устанавливаем модуль connexion для разработки WEB сервисов
REST API
pip install connexion[swagger-ui]
Добавим вызовы RestApi с свяжем их со специальных классом - репозиторий.
Url (endpoint) | Описание |
---|---|
/api/acts/ | GET, получить список всех актов |
/api/acts/uid | GET, получить карточку конкретного акта по его коду |
/api/executors/ | GET, получить список всех исполнителей |
/api/executors/uid | GET, получить карточку конкретного исполнителя по его коду |
/api/contractors/ | GET, получить список всех застройщиков |
/api/contractors/uid | GET, получить карточку конкретного застройщика по его коду |
Так же, добавим модульные тесты и тесты на конвертацию данных в формат json Для проверки работы RestApi в класс репозиторий добавим демо данные. Удобней это сделать при помощи фабричного метода. Пример:
def create(is_demo = False):
"""
Фабричный метод
"""
main = repo()
if is_demo == True:
main.load_demo()
else:
main.load()
return main
Для работы с API создадим специальный yaml файл в котором создадим описание всех точек вызова. Для проверки, запускаем: http://127.0.0.1:8080/api/ui
- Доработать yaml файл. Включить в него описания вызовов для остальных сервисов. Пример: https://swagger.io/docs/specification/basic-structure/
- Найти ошибку в коде. При сериализации объекта executor значения поля
contractor
выглядит следующим образом:
"contra\u0441tor": {
"description": "",
"guid": "3de8f7f4-7e4b-4d91-8bfd-a3cfd00bfbf4",
"name": "test2"
}
- Написать простой Js код, который будет отображать данные от
RestApi
. Желательно, использовать систему генерации Js кода, например: https://github.com/RicoSuter/NSwag - Доработать метод
load_demo
класса repo. Добавить в него генерацию всех сущностей. - Доработать модель акта. Добавить свойство - объект капитального строительства (ОКС), building
Подключение:
clickhouse-client --host rc1a-7ut3ob6t69958voj.mdb.yandexcloud.net --secure --user user --database db --port 9440 --ask-password
При удачном подключении:
rc1a-7ut3ob6t69958voj.mdb.yandexcloud.net :)
Таблицы:
Наименование | Описание | SQL запрос |
---|---|---|
buildings |
Таблица всех объектов капитального строительства (ОКС) | create table buildings(id UUID, name String, description String, primary key[id]) engine = MergeTree; |
statuses |
Таблица статусов | create table statuses(id UUID, code Int, name String, description String, primary key[id]) engine = MergeTree; |
executors |
Таблица исполнителей | create table executors(id UUID, name String, description String, contractor_id UUID not null, primary key[id]) engine = MergeTree; |
contractors |
Таблица застройщиков | create table contractors(id UUID, parent_id UUID, name String,description String, primary key[id]) engine = MergeTree; |
acts |
Таблица с основной информацией по актам | create table acts(id UUID, building_id UUID not Null, executor_id UUID, period DateTime not null, primary key[id]) engine = MergeTree; |
acts_contractor_links |
Таблица связи акта с застройщиками | create table acts_contractors_links(id UUID, period DateTime not null, contractor_id UUID not null, primary key[id]) engine = MergeTree; |
acts_status_links |
Таблица связи акта со статусом | create table acts_status_links(id UUID, period DateTime not null, status_code Int not null, comments String, amount Float32 default(0), primary key[id]) engine = MergeTree |
Проверка:
show tables;
┌─name───────────────────┐
│ acts │
│ acts_contractors_links │
│ acts_status_links │
│ buildings │
│ contractors │
│ executors │
│ statuses │
└────────────────────────┘
Добавим статусы:
insert into statuses(id, name, code, description) select generateUUIDv4(),'preparation',1, 'Все документы для начала строительства / этапа собраны и проверены. Разрешения выданы';
insert into statuses(id, name, code, description) select generateUUIDv4(),'start',2, 'На объекте начаты строительные работы. Представители субподрядчика прибыли на объект';
insert into statuses(id, name, code, description) select generateUUIDv4(),'passing',3, 'Очередная проверка пройдена без замечаний со стороны контроллирующих органов';
insert into statuses(id, name, code, description) select generateUUIDv4(),'failure',4, 'Очередная проверка пройдена с замечаниями. Необходимо устранить все замечания';
insert into statuses(id, name, code, description) select generateUUIDv4(),'finish',5, 'Все замечания устранены. Все проверки пройдены. Оплату можно проводить';
- Доработать таблицу
statuses
. Исключить возможность дублирования записей по полям: name,code. Сделать в виде SQL скрипта в котором сразу же включить проверку. - Изенить таблицу
acts_status_links
. Сделать связь со статусом не по UUID , а по коду Int. Сделать в виде SQL скрипта.
- Установим пакет
pip install clickhouse-driver
- Создадим отдельный класс для работы с базой данных
proxy
. - Так же, добавим для проверки модульный тест
- Для всех моделей добавим метод
__str__
и в каждом методе определим SQL команду для вставки данных.
Пример:
def __str__(self):
"""
Сформировать SQL запрос на вставку данных
"""
sql = "insert into executors(id, name, description, contractor_id) values('%s', '%s', '%s', '%s')" % (self.id.toJSON(), self.name, self.description, self.contraсtor.id.toJSON())
return sql
- Создадим отдельный класс c автозапуском. Последовательно в нем реализуем генерацию всех моделей с засечкой времени выполнения.
- Для фиксации времени выполнения добавим новый метод в модель
period
Пример:
-> Генерация записей: buildings
Старт: 2023-05-08 13:39:54
Сформировано успешно 100 записей за 15.305252 сек.
-> Генерация записей: contractors
Старт: 2023-05-08 13:40:10
Сформировано успешно 100 записей за 15.028041 сек.
-> Генерация записей: executors
Старт: 2023-05-08 13:40:25
Сформировано успешно 100 записей за 14.503743 сек.
-> Генерация записей: acts
Старт: 2023-05-08 13:40:39
Сформировано успешно 50 записей за 555.550269 сек.
-> Генерация записей: acts_status_links
Старт: 2023-05-08 13:49:55
Сформировано успешно 25 записей за 357.434632 сек.
Генерация данных завершена за 959.044415 сек.
- Вывести информацию в виде (создать SQL запрос):
Код акта | Дата ввода документа | Сумма штрафа | Дата смены статуса | Наименование статуса |
---|
- Вывести информация в виде (составить SQL запрос):
Код акта | Дата ввода документа | Сумма штрафа | Дата текущего статуса | Наименование текущего статуса |
---|
- Доработать код загрузки данных в файле Repo.py. Метод:
def load(self):
"""
Подключиться к базе данных
"""
self.__proxy = db_proxy()
self.__proxy.open()
Критерии:
- Последний рабочий статус акта
failure
(код 4)- Имеют разные объекты строительства (ОКС)
with cte_acts as
(
-- Список проблемных актов
select id as act_id from ( select id, argMax(status_code, period) as status_code from acts_status_links group by id) as tt where tt.status_code = 4
),
cte_buildings as
(
-- Список проблемных застройщиков
select t2.id as building_id, t2.name as building_name
from acts as t1
inner join cte_acts as tt on tt.act_id = t1.id
inner join buildings as t2 on t1.building_id = t2.id
group by t2.id, t2.name
),
cte_quantity_acts as
(
-- Количество актов в работе по каждому застройщику
select t1.building_id, count(*) as cnt_all from acts as t1 where t1.building_id in ( select building_id from cte_buildings)
group by t1.building_id
),
cte_quantity_failure_acts as
(
-- Количество актов по застройщикам, которые имеют проблемы
select t1.building_id as building_id, count(*) as cnt_failure, sum(tt.amount) as amount from acts as t1
left join ( select id as act_id, argMax(amount, period) as amount from acts_status_links where status_code = 4 group by id ) as tt on tt.act_id = t1.id
where t1.id in (select act_id from cte_acts)
group by t1.building_id
)
select concat('http://localhost:8080/api/contractors/', toString(t1.building_id)) as link, t1.building_name as name, t2.cnt_all as qauntity, t3.cnt_failure as failure, t3.amount from cte_buildings as t1
left join cte_quantity_acts as t2 on t1.building_id = t2.building_id
left join cte_quantity_failure_acts as t3 on t3.building_id = t1.building_id
order by t2.cnt_all, t3.cnt_failure, t3.amount desc;
Выводим информацию в следующем виде:
Ссылка на застройщика | Наименование застройщика | Количество актов в работе | Количество актов с замечаниями | Сумма предполагаемого штрафа,руб |
---|
Результат выполнения запроса:
┌─link───────────────────────────────────────────────────────────────────────┬─name──────────┬─qauntity─┬─failure─┬─t3.amount─┐
│ http://localhost:8080/api/contractors/46a11a3a-7657-4d96-8a3f-7f0eaaae7013 │ building № 53 │ 2 │ 2 │ 1976 │
│ http://localhost:8080/api/contractors/58a7a2a0-88cd-4587-aba3-8176aef0f9d1 │ building № 34 │ 2 │ 2 │ 1612 │
│ http://localhost:8080/api/contractors/33844ae0-8e6c-4f91-8bcc-c0dc9a4abe8a │ building № 71 │ 2 │ 2 │ 270 │
│ http://localhost:8080/api/contractors/7b96ee1f-6b18-4d7a-86fc-bdb87c206ce6 │ building № 29 │ 3 │ 2 │ 1948 │
│ http://localhost:8080/api/contractors/4c469434-3a81-483d-9b51-a8bf402d066c │ building № 19 │ 5 │ 2 │ 1052 │
│ http://localhost:8080/api/contractors/66c22827-db8b-401f-b003-d875993bca44 │ building № 42 │ 7 │ 2 │ 424 │
└────────────────────────────────────────────────────────────────────────────┴───────────────┴──────────┴─────────┴───────────┘
Реализация выполнена в виде наследования от общего класса. Дорабатываем yaml файл. Запускаем: http://127.0.0.1:8080/api/ui/. Получаем: