Ваша первая
мастерская.
Система заявок, которую можно запустить, разобрать и превратить в своё приложение. Четыре понятные части — от бизнес-правил до оформления.

Скачайте. Запустите. Попробуйте.
Нужны Python 3.12+ и интернет. Распакуйте архив, откройте терминал в папке workshop — рядом должны быть app.py и requirements.txt.
macOS / Linux
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python app.pyWindows PowerShell
py -3 -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
.venv\Scripts\python.exe app.pyВ остальных командах заменяйте python на .venv\Scripts\python.exe. Активация окружения не требуется.
После запуска откройте http://127.0.0.1:8088. Ссылка ведёт на приложение на вашем компьютере.
- Добавьте «Отремонтировать светильник».
- Нажмите «Завершить» — счётчики обновятся.
- Выберите «Готово», затем «Вернуть в работу».
- Остановите сервер через Ctrl+C и снова выполните
python app.py. Заявка останется.
Четыре части одного продукта.
| Часть | Файл | Ответственность |
|---|---|---|
| Модуль | modules/requests.py | Данные и правила работы с заявками |
| Решение | solution.py | Сборка модулей и запуск ядра |
| Шаблон | templates/index.html | Структура страницы; значения готовит presentation.py |
| Ассет-пак | asset_pack/static/ | CSS и SVG-иконка |
| Веб-интерфейс | app.py | HTTP, обработка форм и раздача файлов |
presentation.py + шаблон + ресурсы → готовый экран
Ядро управляет модулями, зависимостями и событиями. HTTP и SQLite подключены явно стандартными библиотеками Python. «Решение» здесь — собранное приложение, «шаблон» — HTML. Это исходный проект, а не опубликованный товар GoChain.
Начните с действия пользователя.
Модуль проверяет длину названия, записывает заявку и отправляет событие. Web-интерфейс вызывает requests.create(title), а не повторяет эти правила.
boot()готовит таблицу и регистрирует сервис черезengine.provide().create()принимает 3–120 символов и возвращает номер.set_status()переключаетnew↔done.list()возвращает список обычных словарей Python.
Запуск без браузера:
python run_module.pyИспользуется временная база, ваши заявки не меняются.
Полный код — modules/requests.py
"""Request storage and business rules: usable from a website, CLI or tests."""
from contextlib import contextmanager
from pathlib import Path
import sqlite3
from yeti3_core import Module, ModuleManifest
class RequestsModule(Module):
manifest = ModuleManifest(
id="workshop.requests", name="Заявки мастерской", version="1.0.0",
provides=("workshop.requests",),
)
def __init__(self, database: Path):
self.database = Path(database)
@contextmanager
def connect(self):
connection = sqlite3.connect(self.database, timeout=5)
connection.row_factory = sqlite3.Row
try:
with connection:
yield connection
finally:
connection.close()
def boot(self, engine):
self.engine = engine
self.database.parent.mkdir(parents=True, exist_ok=True)
with self.connect() as connection:
connection.execute("""CREATE TABLE IF NOT EXISTS requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'new' CHECK(status IN ('new','done')),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)""")
engine.provide("workshop.requests", self, module_id=self.manifest.id)
def list(self, status="all"):
if status not in {"all", "new", "done"}:
raise ValueError("Неизвестный фильтр")
with self.connect() as connection:
if status == "all":
rows = connection.execute("SELECT * FROM requests ORDER BY id DESC")
else:
rows = connection.execute(
"SELECT * FROM requests WHERE status=? ORDER BY id DESC", (status,))
return [dict(row) for row in rows]
def create(self, title):
title = title.strip()
if not 3 <= len(title) <= 120:
raise ValueError("Название должно содержать от 3 до 120 символов")
with self.connect() as connection:
cursor = connection.execute("INSERT INTO requests(title) VALUES (?)", (title,))
request_id = cursor.lastrowid
# Publish only after the transaction has committed.
self.engine.events.emit("workshop.request.created", request_id)
return request_id
def set_status(self, request_id, status):
if status not in {"new", "done"}:
raise ValueError("Неизвестный статус")
with self.connect() as connection:
result = connection.execute(
"UPDATE requests SET status=? WHERE id=?", (status, request_id))
if result.rowcount != 1:
raise LookupError("Заявка не найдена")
Попробуйте: поменяйте название заявки в run_module.py. Затем передайте строку из двух символов — получите ошибку проверки.
Соедините модули в приложение.
Все модули регистрируются до engine.boot(). Ядро учитывает зависимости, а capabilities.get() возвращает сервис по имени.
"""A solution assembles modules and exposes the services used by the interface."""
from pathlib import Path
from yeti3_core import Engine
from modules.requests import RequestsModule
from modules.activity import ActivityModule
def create_solution(database: Path):
engine = Engine()
engine.register(RequestsModule(database))
engine.register(ActivityModule())
engine.boot()
requests = engine.capabilities.get("workshop.requests")
return engine, requests
Второй модуль реагирует на создание заявки. В терминале появляется Создана заявка №1.
Модуль событий — modules/activity.py
"""A second module shows dependencies and event subscriptions."""
from yeti3_core import Module, ModuleManifest
class ActivityModule(Module):
manifest = ModuleManifest(
id="workshop.activity", name="События мастерской", version="1.0.0",
requires=("workshop.requests",),
)
def boot(self, engine):
engine.events.subscribe("workshop.request.created", self.on_created)
def on_created(self, request_id):
print(f"Создана заявка №{request_id}", flush=True)
def shutdown(self, engine):
engine.events.unsubscribe("workshop.request.created", self.on_created)
Попробуйте: измените сообщение в on_created(), перезапустите приложение и создайте заявку. Для нового модуля задайте уникальный id и зарегистрируйте его до запуска. В requires указываются идентификаторы модулей, а не pip-пакеты.
Меняйте экран без изменения правил.
presentation.py заполняет $title, $cards, $total, $pending и $completed. Используется простой string.Template из Python.
Полный HTML-шаблон
<!doctype html>
<html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>$title · YETI³</title><link rel="icon" href="/static/mark.svg"><link rel="stylesheet" href="/static/workshop.css"></head>
<body><header><a class="brand" href="/"><img src="/static/mark.svg" alt="" width="30" height="30"> $title</a><span>НА ОСНОВЕ YETI³ CORE</span></header>
<main><div class="eyebrow">ВАШЕ ДЕЛО. ВСЁ ПОД РУКОЙ.</div><h1>От первой заявки<br>до <em>готовой работы.</em></h1><p class="intro">Маленькая мастерская с понятным порядком: добавляйте задачи, отмечайте результат и возвращайтесь к работе.</p>
<div class="stats"><div><b>$total</b><span>Всего заявок</span></div><div><b>$pending</b><span>В работе</span></div><div><b>$completed</b><span>Завершено</span></div></div>
<div class="workspace"><section class="create"><div class="eyebrow">01 / НОВАЯ ЗАЯВКА</div><h2>Что нужно сделать?</h2>$message
<form method="post" action="/requests"><input type="hidden" name="csrf" value="$csrf">
<label for="title">Название заявки</label><input id="title" name="title" minlength="3" maxlength="120" required placeholder="Например, отремонтировать светильник">
<small>От 3 до 120 символов. Данные сохранятся после перезапуска.</small><button>Добавить заявку ↗</button></form>
</section><section class="board"><div class="eyebrow">02 / РАБОЧИЙ СПИСОК</div><h2>Заявки мастерской</h2>
<nav aria-label="Фильтр заявок"><a class="$all_active" href="/">Все</a><a class="$new_active" href="/?status=new">В работе</a><a class="$done_active" href="/?status=done">Готово</a></nav>$cards</section></div>
<footer>Учебный проект · Python + YETI³ Core + SQLite · Данные хранятся на вашем компьютере</footer></main></body></html>
Попробуйте: в HTML замените «Что нужно сделать?» на «Новая работа» и обновите браузер. Название проекта меняется в presentation.py; после изменения Python перезапустите приложение.
Пользовательский текст обрабатывается через html.escape(). Сохраняйте это правило для новых полей. Буквальный знак доллара в шаблоне пишется как $$.
Оформление для разных страниц.
В asset_pack/static лежат workshop.css и mark.svg. Измените --accent в CSS или замените SVG — бизнес-логика останется прежней. Основной запуск читает ресурсы напрямую.
Отдельный пример установки через SDK
python -m pip install --index-url https://engine.yeti3.ru/sdk/simple/ yeti3-assets==0.3.1
python install_assets.pyРезультат: installed-assets/static и запись владельца в installed-assets/.yeti3/ownership. Исходные файлы не меняются.
Манифест описывает состав
[module]
id = "workshop.assets"
name = "Оформление мастерской"
version = "1.0.0"
engine = ">=0.3,<0.4"
entrypoint = "module:WorkshopAssets"
[assets]
static = ["static"]
Код установки — install_assets.py
"""Install files through the SDK adapter into a separate demo directory."""
from pathlib import Path
from yeti3_assets import FilesystemAssetAdapter
from yeti3.sdk.manifest import load_manifest, validate_manifest
from yeti3.sdk.installer.registry import InstalledModule
root = Path(__file__).resolve().parent
manifest = load_manifest(root / 'asset_pack/yeti3-module.toml')
validate_manifest(manifest)
module = InstalledModule(manifest=manifest, source=str(root / 'asset_pack'))
adapter = FilesystemAssetAdapter(root / 'installed-assets')
try:
adapter.install(module)
except Exception as error:
raise SystemExit(f'Установка не выполнена: {error}. Для повторного опыта выберите новую пустую папку.')
print('Готово: installed-assets/static/workshop.css и mark.svg')
print('SDK записал владельца файлов в installed-assets/.yeti3/ownership/')
Попробуйте: повторите установку. Адаптер откажется перезаписывать файлы. Для следующего опыта укажите новую пустую папку в скрипте. Здесь вызывается файловый адаптер; покупка, доставка и активация GoChain в пример не входят.
Превратите пример в свой проект.
- Оформление: измените название в Python, заголовок в HTML, акцентный цвет в CSS.
- Правила: увеличьте минимальную длину названия с 3 до 5 в модуле, HTML-атрибуте
minlengthи подсказке. Добавьте тест на четыре символа. - Новая функция: создайте ещё один модуль по образцу
ActivityModule, задайте зависимость и получите сервис черезengine.capabilities.get("workshop.requests").
Для нового столбца потребуется миграция: CREATE TABLE IF NOT EXISTS не изменяет существующую таблицу. Сначала сделайте копию данных.
Перед публичным запуском
Для многопользовательского сайта нужны вход, права на заявки, миграции, резервное копирование и боевой HTTP-сервер за HTTPS. Не открывайте учебный http.server в интернет. Бизнес-модули можно сохранить и подключить к выбранному веб-фреймворку.
Тесты идут вместе с проектом.
python -m unittest discover -s tests -v11 проверок: данные после перезапуска, правила и статусы, события, HTML и SQL, HTTP-формы, доступ к статическим файлам. Проверка ассет-адаптера пропускается до установки yeti3-assets.
Порт 8088 занят
Запустите python app.py --port 8090, откройте http://127.0.0.1:8090.
Python не находит yeti3_core
Повторите установку из requirements.txt тем же Python, которым запускаете приложение. На Windows используйте полный путь из инструкции.
После перезапуска форма не отправляется
При запуске меняется защитный токен. Обновите страницу перед отправкой.
Где данные и как сделать копию?
В data/workshop.sqlite3. Остановите приложение и скопируйте файл. Восстанавливайте копию при остановленном приложении. Свой путь: python app.py --database /path/to/app.sqlite3.