Спроєктувати таблиці свого продукту, створити їх у базі, додати перші справжні записи й віддати їх назовні окремою адресою, яку зможе відкрити будь-яка програма.
Замість <логін> підстав свій — той, який тобі видали на першому занятті. Логін має вигляд bohdan.g9: ім’я, крапка, номер групи. Кабінет — сторінка, де видно, які пункти автоперевірки тобі вже зараховано. API (вимовляють «ей-пі-ай») — набір адрес, за якими сервер віддає не сторінку для людини, а дані для іншої програми.
Ти опишеш на папері таблицю свого продукту — які в ній поля, які з них обов’язкові, який тип у кожного, — а потім створиш цю таблицю в базі даних однією командою CREATE TABLE. У базу ти додаси п’ять справжніх записів свого сервісу.
Після цього твоє API навчиться віддавати ці записи. Одна адреса API називається ендпоінт. Дані за нею передають у форматі JSON: це текст, у якому один об’єкт записаний у фігурних дужках парами «назва»: значення, а список об’єктів — у квадратних дужках через кому. За адресою на кшталт /u/<логін>/api/habits будь-хто з інтернету отримає такий текст із твоїми записами.
Наприкінці автоперевірка порахує записи в базі, порахує об’єкти у відповіді й порівняє два числа — вони мають збігтися.
Дані сервісу справді можна зберігати у звичайному текстовому файлі: один рядок — один запис, поля через кому. Для двадцяти рядків це працює, і для першої спроби так роблять. Проблеми починаються далі, і їх п’ять.
База даних — програма, яка зберігає дані у вигляді таблиць і відповідає на запити до них. На кожен із цих п’яти пунктів у бази є відповідь:
database is locked;Ми беремо SQLite — базу даних, яка тримає все в одному файлі на диску й не потребує окремої служби. Служба — це програма, яку операційна система запускає сама й тримає ввімкненою у фоні, поки працює сервер. Іншим базам даних така програма потрібна, SQLite обходиться без неї. Твоя база лежатиме у файлі ~/data/app.db. Саме через це її зручно копіювати й відновлювати: на занятті 11 ти робитимеш із неї резервні копії звичайною командою копіювання файлу.
Твій сервіс зберігає записи в текстовому файлі. Записів стало 40 000, і сторінка, яка показує записи за один вибраний день, відкривається чотири секунди. Що зміниться, якщо перекласти ті самі дані в базу?
Дані в базі лежать у таблицях. Розберемо три слова, які далі будуть у кожному реченні:
Приклад, який ти бачив: журнал успішності. Стовпці там — прізвище, дата, оцінка. Рядок — одна виставлена оцінка. Кожен рядок має ті самі стовпці, що й решта; порожні клітинки бувають, а от «у цього учня є ще один стовпчик, якого нема в інших» — не буває. Це і є головна властивість таблиці.
Схема — повний перелік таблиць твого продукту, їхніх стовпців, типів і правил. Схему пишуть до того, як створювати таблиці: переробити її потім, коли в базі вже є дані, значно складніше.
Назву таблиці пиши в однині, латиницею, малими літерами: habit, game, plant. Латиниця тому, що ця назва потрапляє в кожен запит і в код програми. Однина тому, що один рядок — це одна звичка.
Одна з таблиць у тебе головна — та, записи якої показує список сервісу. Її назву ти запишеш у project.yaml: це файл-опис твого проєкту, у якому зібрані всі його назви й адреси, і такий файл називають маніфестом. Саме цю таблицю рахуватиме автоперевірка.
| Продукт | Головна таблиця | Стовпці |
|---|---|---|
| Щоденник звичок | habit | id, title, day, minutes, note |
| Список пройдених ігор | game | id, title, platform, finished_on, rating |
| Догляд за кімнатними рослинами | plant | id, name, place, watered_on, days_between |
Ти робиш каталог кімнатних рослин. Для кожної рослини треба зберігати назву й місце, а ще історію поливань — кілька дат на одну рослину. Як це розкласти по таблицях?
Первинний ключ (англійською primary key) — стовпець, значення якого не повторюється в таблиці й ніколи не буває порожнім. За ним база знаходить конкретний рядок, і за ним на цей рядок посилаються інші таблиці.
Найпростіший і найнадійніший варіант — окремий числовий стовпець id, який база заповнює сама:
id INTEGER PRIMARY KEY AUTOINCREMENTНаступне число — 1, 2, 3 — база ставить сама вже через INTEGER PRIMARY KEY: тобі не треба ані передавати це значення, ані пам’ятати, яке було останнім. Слово AUTOINCREMENT додає до цього одне правило: номери видалених рядків більше не використовуються повторно. Без нього база може віддати новому рядку номер, який раніше належав видаленому.
Може виникнути думка взяти ключем назву — вона ж і так унікальна. Дві причини цього не робити: назви повторюються (у щоденнику звичок «Пробіжка» буде десятки разів, і це нормально), а ще назву іноді виправляють — після виправлення всі посилання на старе значення стають хибними. Номер id не змінюється ніколи.
Тепер про поля, які лишили незаповненими. NULL — окреме значення, яке означає «сюди нічого не записали». Воно не дорівнює нулю й не дорівнює порожньому рядку: нуль хвилин — це відомий факт, а NULL — відсутність факту.
NOT NULL — правило для стовпця: тут порожнього значення бути не може. Його ставлять на поля, без яких запис не має сенсу. У щоденнику звичок це назва й дата: запис без дати неможливо ані показати в списку, ані відфільтрувати. Якщо спробувати додати рядок без такого поля, база відмовить і поверне помилку NOT NULL constraint failed.
Поруч є ще два правила, які знадобляться далі:
UNIQUE-стовпців у таблиці може бути кілька, а первинний ключ один;minutes INTEGER NOT NULL DEFAULT 0.На кроці 2 практики ви з сусідом по парті поставите одне одному питання «а якщо два записи з однаковою назвою?» Правильна відповідь: нічого не станеться, вони різняться значенням id. Якщо ти зробив ключем назву — станеться помилка, і другий запис у базу не потрапить.
У таблиці game ти зробив первинним ключем поле title. Через місяць виявилося, що дві різні гри в тебе записані з однаковою назвою, а ще в одній назві є помилка, яку хочеться виправити. Що буде?
Тип даних — вид значення, яке лежить у стовпці. У SQLite їх небагато:
INTEGER — ціле число: хвилини, кількість, оцінка, номер;REAL — число з дробовою частиною: вага, температура;TEXT — текст будь-якої довжини: назва, опис, дата;BLOB — двійкові дані, наприклад вміст картинки. Нам сьогодні не потрібен.Тип обирають за тим, що ти збираєшся з цим значенням робити. Хвилини записують числом, бо з числами рахують суму й середнє. Якщо записати «18 хв» текстом, база не зможе їх додати: доведеться в Python відрізати два останні символи в кожному рядку.
Окремого типу для дати в SQLite немає. Через це дату записують текстом у певному форматі, і саме тут найчастіше помиляються. Формат такий: рік-місяць-день, чотиризначний рік і двозначні місяць та день, 2026-09-06. Причина проста: у такому записі порівняння тексту дає той самий результат, що й порівняння дат. Рядок "2026-09-06" більший за "2026-08-31" і як текст, і як дата, тому сортування й відбір за проміжком часу працюють без жодних перетворень.
Якщо писати 06.09.2026, база порівнюватиме спочатку день. У списку, впорядкованому за такою «датою», 06 вересня опиниться поруч із 06 березня, а відбір «за останній тиждень» доведеться писати самому в Python.
І ще одна особливість саме SQLite: вона не забороняє записати текст у стовпець, оголошений як INTEGER. Тип тут — це домовленість, яку тримає твій код. А ось правила NOT NULL, UNIQUE, PRIMARY KEY і CHECK база перевіряє по-справжньому й порушити їх не дасть. CHECK — умова, яку база перевіряє під час кожного запису, наприклад CHECK (minutes >= 0): від’ємної кількості хвилин після цього в таблиці не з’явиться.
Ти зберігав дати у вигляді 06.09.2026. Тепер треба показати записи за останній тиждень і впорядкувати їх від найновіших. Що станеться з таким форматом?
SQL — мова запитів до бази. Нею ти кажеш, який результат тобі потрібен; як саме його дістати з файлу, база вирішує сама. Команди традиційно пишуть великими літерами — так їх видно серед назв стовпців; база великі й малі літери в ключових словах не розрізняє.
CREATE TABLE habit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
day TEXT NOT NULL,
minutes INTEGER NOT NULL DEFAULT 0,
note TEXT
);У дужках перелічені стовпці: назва, тип і правила. Стовпець note без NOT NULL — необов’язковий, його можна лишити порожнім.
INSERT INTO habit (title, day, minutes) VALUES ('Пробіжка 2 км', '2026-09-01', 18);Спочатку перелік стовпців, потім значення в тому самому порядку. Текст у одинарних лапках, числа без лапок. Стовпець id не вказуємо — база поставить його сама.
SELECT * FROM habit ORDER BY id DESC;
SELECT id, title, day FROM habit WHERE day >= '2026-09-01' ORDER BY day DESC;
SELECT id, title FROM habit ORDER BY id DESC LIMIT 10;
SELECT COUNT(*) FROM habit;Що тут за що відповідає:
SELECT * — усі стовпці; замість зірочки можна перелічити потрібні через кому;FROM habit — з якої таблиці;WHERE — умова відбору: у результат потраплять лише рядки, для яких вона істинна;ORDER BY day DESC — упорядкувати за стовпцем day, DESC означає від більшого до меншого;LIMIT 10 — віддати не більше десяти рядків. Корисна річ, але саме через неї найчастіше не збігаються числа в автоперевірці;COUNT(*) — рахує рядки. Результат — таблиця з одного рядка й одного стовпця.DELETE FROM habit WHERE id = 7;Без WHERE команда видаляє всі рядки таблиці й нічого не перепитує, тому умову пиши завжди.
Останнє про запити: значення в них ніколи не склеюють із текстом запиту через + або f-рядок. Замість значення ставлять знак питання, а саме значення передають окремим списком:
db.execute("INSERT INTO habit (title, day) VALUES (?, ?)", ("Пробіжка", "2026-09-01"))Так база отримує текст запиту й дані окремо й ніколи не сприймає дані як команду. Що буває інакше, розберемо на занятті 08, коли перевірятимемо сервіси одне одного на слабкі місця в захисті.
У твоїй таблиці 12 рядків. Ендпоінт списку повертає 10 об’єктів, і автоперевірка пише, що числа не збігаються. Який рядок запиту найімовірніше винен?
Нагадування із заняття 02: ендпоінт — це адреса, за якою сервер щось конкретне відповідає. Сьогодні ти додаєш ендпоінт списку — третій після /api/ping і /api/about.
У теці ~/app уже лежить готовий файл db.py. Його редагувати не треба, але треба знати, що в ньому:
connect() — відкриває файл бази. Шлях бере з project.yaml, тому в коді його ніде не пишуть руками;query(sql, params) — виконує SELECT і повертає список словників Python, по одному словнику на рядок. Словник — це набір пар «назва: значення»; значення дістають за назвою, наприклад row["title"]. Назвами тут стають назви стовпців;execute(sql, params) — виконує INSERT, UPDATE або DELETE і повертає id доданого рядка;DB_PATH — готовий шлях до файлу бази. Його зручно надрукувати, щоб побачити, з яким саме файлом працює твій код.Підключити db.py до свого файлу можна двома способами, і сьогодні тобі трапляться обидва. Рядок import db підключає весь файл, і далі до всього в ньому звертаються через крапку: db.query(...), db.DB_PATH. Рядок from db import query підключає лише одну функцію, і тоді пишуть query(...) — без db і крапки. Перш ніж писати виклик, подивись, який із цих двох рядків стоїть на початку файлу, у якому ти працюєш.
Сам ендпоінт після цього займає три рядки:
@app.get("/api/habits")
def list_items():
return query("SELECT * FROM habit ORDER BY id DESC")Рядок, що починається зі знака @ й стоїть просто над функцією, називають декоратором: він прив’язує функцію до адреси. @app.get("/api/habits") читається так: «коли хтось запитає цю адресу методом GET, виклич функцію, написану нижче». GET — вид запиту, яким просять дані; саме такий запит робить браузер, коли ти відкриваєш адресу.
FastAPI — бібліотека, якою написаний твій main.py. Вона зіставляє адресу запиту з потрібною функцією й перетворює те, що функція повернула, на JSON. Наша функція повертає список словників, і у відповіді він стає масивом JSON: квадратні дужки, всередині об’єкти у фігурних дужках через кому.
[{"id": 5, "title": "Читання", "day": "2026-09-04", "minutes": 40, "note": ""},
{"id": 4, "title": "Пробіжка 2 км", "day": "2026-09-03", "minutes": 17, "note": "дощ"}]Звідси й береться критерій заняття: кількість об’єктів у цьому масиві дорівнює кількості рядків, які повернув SELECT. Якщо в запиті немає ані WHERE, ані LIMIT, вона дорівнює COUNT(*) усієї таблиці.
Дві речі, без яких критерій не зарахується. Перша: адресу свого ендпоінта треба записати в маніфест project.yaml. Це єдине місце, де вона зафіксована, і саме звідти її беруть і викладач, і скрипти перевірки — самі вони назв не вгадують.
Друга: твій сервіс тримає ввімкненим uvicorn — програма, яка приймає запити з мережі й передає їх функціям із main.py. Файл main.py вона читає один раз, під час запуску, тому після кожної зміни файлу службу треба перезапустити. Службами в Linux керує systemd — частина системи, яка запускає програми у фоні й стежить, щоб вони працювали; команди їй дають через systemctl:
systemctl --user restart club-apiІ ще одне, що знадобиться вже в наступному питанні. Крім даних сервер повертає на кожен запит код відповіді — тризначне число, яким він каже, чим усе скінчилося. Сьогодні трапляться три: 200 — усе гаразд, відповідь у тілі; 404 — сервер не знає такої адреси; 500 — адреса є, але функція під час роботи завершилася з помилкою.
Ти дописав ендпоінт списку, зберіг main.py і одразу відкрив адресу в браузері. Сервер відповідає 404. Найімовірніша причина?
| Слово | Що означає |
|---|---|
| база даних | Програма, яка зберігає дані таблицями й відповідає на запити до них |
| SQLite | База, що тримає все в одному файлі й не потребує окремої служби |
| SQL | Мова запитів до бази |
| таблиця, рядок, стовпець | Набір записів одного виду, один запис, одна характеристика |
| схема | Перелік таблиць, їхніх стовпців, типів і правил |
| первинний ключ | Стовпець, значення якого унікальне й ніколи не порожнє; за ним знаходять рядок |
| INTEGER PRIMARY KEY | Числовий первинний ключ: наступний номер база ставить сама |
| AUTOINCREMENT | Додаткове правило: номери видалених рядків не використовуються повторно |
| NULL | Значення «нічого не записано». Не нуль і не порожній рядок |
| NOT NULL | Правило: у цьому стовпці порожнього значення бути не може |
| UNIQUE, DEFAULT, CHECK | Правила: значення не повторюється; підставити своє, якщо не передали; умова для кожного запису |
| тип даних | Вид значення в стовпці: INTEGER, REAL, TEXT, BLOB |
| CREATE TABLE | Створює таблицю з описаними стовпцями й правилами |
| INSERT | Додає рядок у таблицю |
| SELECT | Дістає рядки; WHERE — умова, ORDER BY — порядок, LIMIT — обмеження кількості |
| COUNT(*) | Рахує кількість рядків |
| DELETE | Видаляє рядки за умовою |
| транзакція | Група дій, яка виконується або повністю, або ніяк |
| індекс | Службова структура, що дає знайти рядок без перегляду всіх |
| PRAGMA table_info | Запит, який показує опис стовпців таблиці |
| API | Набір адрес, за якими сервер віддає дані іншій програмі, а не сторінку людині |
| ендпоінт | Одна така адреса |
| JSON | Текстовий формат передавання даних: об’єкт у фігурних дужках, список — у квадратних |
| код відповіді | Число, яким сервер каже, чим скінчився запит: 200, 404, 500 |
| маніфест | Файл project.yaml, у якому записані назви й адреси проєкту |
| служба | Програма, яку система тримає ввімкненою у фоні. Керує службами systemd, команда — systemctl |
| порт | Номер, за яким система розрізняє програми, що приймають запити на одній машині |
Кроки виконуй послідовно. Приклади написані для щоденника звичок і таблиці habit — скрізь підставляй назву своєї таблиці й свої стовпці.
ssh ЛОГІН@91.219.61.4Замість ЛОГІН — свій, наприклад bohdan.g9. Пароль під час введення не показується: ні крапок, ні зірочок. Набирай і натискай Enter.
Схему пиши на папері до того, як відкриєш редактор: на аркуші видно всю таблицю одразу, і виправити її там швидше, ніж переробляти вже створену. Випиши на аркуші:
INTEGER чи TEXT), обов’язковий він чи ні;id — це первинний ключ.Дві перевірки, які треба пройти до того, як писати код. Перша: запис без якого поля не має сенсу? Ці поля отримують NOT NULL. Друга: у чому вимірюється кожне числове поле — у хвилинах, штуках, гривнях? Одиницю виміру пиши в назві стовпця або в коментарі, інакше через місяць не згадаєш.
Далі поміняйтеся аркушами з сусідом і поставте одне одному два питання:
Якщо на друге питання відповіді немає, у схемі бракує стовпця з датою у форматі 2026-09-06. Додай його зараз, поки таблиці ще не існує.
project.yamlnano ~/app/project.yamlЦе той самий маніфест — файл-опис проєкту, який ти почав заповнювати на занятті 01. Знайди в ньому розділ database і заповни його. Приклад для щоденника звичок:
database:
file: app.db
main_table: habit
main_table_pk: id
main_table_required: [title, day]
secondary_tables: []Що означає кожен рядок:
file — ім’я файлу бази в теці ~/data. Лишай app.db;main_table — назва головної таблиці, записи якої показує список;main_table_pk — назва стовпця з первинним ключем;main_table_required — перелік стовпців, які мають бути NOT NULL. Саме цей список звіряє автоперевірка зі справжньою таблицею;secondary_tables — решта таблиць, якщо вони в тебе є. Порожній список теж підходить.Зберегти — Ctrl+O, потім Enter. Вийти — Ctrl+X. Пам’ятай про відступи. Цей файл написаний у форматі YAML: у кожному рядку назва, двокрапка й значення, а вкладеність показана відступом зліва. Один рівень вкладеності — два пробіли; табуляція замість пробілів ламає файл.
CREATE TABLEnano ~/app/init_db.pyФайл новий, він буде порожній. Набери в ньому:
"""Створює таблиці. Можна запускати скільки завгодно разів."""
import db
SCHEMA = """
CREATE TABLE IF NOT EXISTS habit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
day TEXT NOT NULL,
minutes INTEGER NOT NULL DEFAULT 0,
note TEXT
);
"""
conn = db.connect()
conn.executescript(SCHEMA)
conn.commit()
print("таблиці на місці, база:", db.DB_PATH)Розбір коду:
import db — підключаємо готовий файл db.py із тієї самої теки. Після такого підключення до всього в ньому звертаємося через крапку: db.connect(), db.DB_PATH. Шлях до бази він знає сам, бо читає project.yaml;SCHEMA — звичайний рядок Python у потрійних лапках, усередині якого лежить текст SQL. Потрійні лапки дозволяють писати кілька рядків;IF NOT EXISTS — якщо таблиця вже є, команда нічого не робить і не видає помилки. Тому файл можна запускати повторно;executescript — виконує весь текст SQL цілком, а не один запит;commit — підтверджує транзакцію, тобто записує зміни на диск остаточно.cd ~/app && /opt/club/venv/bin/python init_db.pyДовгий шлях /opt/club/venv/bin/python — це Python зі спільного оточення групи, у якому вже стоять потрібні бібліотеки. Звичайний python3 їх не побачить.
У відповідь має надрукуватися шлях до бази. Перевір, що файл справді з’явився:
ls -l ~/data/app.dbФайл займе близько 12 кілобайтів — рівно 12288 байтів. Для порожньої таблиці це нормально: база одразу відводить місце під службові структури. Після п’яти записів число буде те саме, і це теж нормально.
cd ~/app && /opt/club/venv/bin/python -c 'import db
for r in db.query("PRAGMA table_info(habit)"):
print(r)'PRAGMA table_info — службовий запит SQLite, який показує опис стовпців таблиці. Ти побачиш приблизно таке:
{'cid': 0, 'name': 'id', 'type': 'INTEGER', 'notnull': 0, 'dflt_value': None, 'pk': 1}
{'cid': 1, 'name': 'title', 'type': 'TEXT', 'notnull': 1, 'dflt_value': None, 'pk': 0}
{'cid': 2, 'name': 'day', 'type': 'TEXT', 'notnull': 1, 'dflt_value': None, 'pk': 0}
{'cid': 3, 'name': 'minutes', 'type': 'INTEGER', 'notnull': 1, 'dflt_value': '0', 'pk': 0}
{'cid': 4, 'name': 'note', 'type': 'TEXT', 'notnull': 0, 'dflt_value': None, 'pk': 0}Дивись на дві колонки. pk має дорівнювати 1 рівно в одного стовпця, а notnull — дорівнювати 1 в усіх стовпцях, які ти перелічив у main_table_required. Саме це звірятиме викладач на розборі.
У рядку id стоїть notnull: 0 — так і має бути. Стовпець INTEGER PRIMARY KEY порожнім не буває: якщо значення не передали, база сама ставить наступний номер. З ключем іншого типу так не виходить — у SQLite, наприклад, у стовпець title TEXT PRIMARY KEY порожнє значення записати можна. Це ще одна причина робити ключем числовий id.
nano ~/app/seed.py"""Додає перші записи. Запускати один раз, інакше будуть дублікати."""
import db
rows = [
("Пробіжка 2 км", "2026-09-01", 18, "стадіон біля школи"),
("Англійська, слова", "2026-09-01", 25, "40 нових слів"),
("Гітара, вправа на бій", "2026-09-02", 30, ""),
("Пробіжка 2 км", "2026-09-03", 17, "дощ, бігли коротше"),
("Читання", "2026-09-04", 40, "60 сторінок"),
]
for row in rows:
db.execute(
"INSERT INTO habit (title, day, minutes, note) VALUES (?, ?, ?, ?)",
row,
)
print("додано записів:", len(rows))Записи мають бути справжні — те, що ти справді робив цього тижня, або справжні дані твого продукту. Рядки «тест1, тест2» тут не підходять: на розборі домашнього завдання викладач читає їх очима, і вигадані дані видно одразу. До того ж на занятті 06 із цими самими записами працюватиме сторонній користувач.
Зверни увагу на знаки питання в запиті. Значення передаються окремим набором — база отримує текст запиту й дані нарізно й ніколи не сприймає дані як частину команди.
cd ~/app && /opt/club/venv/bin/python seed.pyЦя команда має надрукувати додано записів: 5. Якщо замість цього з’явився рядок sqlite3.IntegrityError: NOT NULL constraint failed, у якомусь із рядків списку порожнє обов’язкове поле. У повідомленні після двокрапки вказано, у якому саме стовпці. Виправ рядок і запусти seed.py ще раз.
Тепер порахуємо, що лежить у базі:
cd ~/app && /opt/club/venv/bin/python -c 'import db; print(db.query("SELECT COUNT(*) AS n FROM habit"))'Ця команда має надрукувати [{'n': 5}]. Якщо там 10 — файл seed.py запустився двічі, і в базі дублікати. Як їх прибрати, написано в розділі «Якщо щось не працює».
SELECT із фільтромnano ~/app/check_db.pyimport db
total = db.query("SELECT COUNT(*) AS n FROM habit")[0]["n"]
print("усього записів:", total)
print("за останні 7 днів:")
for row in db.query(
"SELECT id, day, title FROM habit "
"WHERE day >= date('now', '-7 days') "
"ORDER BY day DESC, id DESC"
):
print(" ", row["id"], row["day"], row["title"])cd ~/app && /opt/club/venv/bin/python check_db.pydate('now', '-7 days') — функція SQLite, яка повертає сьогоднішню дату мінус сім днів у вигляді тексту 2026-08-30. Порівняння працює саме тому, що твої дати записані в тому самому форматі. Це і є відповідь на друге з двох питань, які ви ставили одне одному на кроці 2.
query повертає список словників, тому до значень звертаємося за назвою стовпця: row["title"].
nano ~/app/main.pyПриблизно в середині файлу є закоментований блок із заголовком «ЗАНЯТТЯ 04» — три рядки, кожен починається зі знака #. Нижче в тому самому файлі стоїть ще один такий блок, «ЗАНЯТТЯ 05»; його не чіпай, він для наступного заняття. У трьох рядках блоку «ЗАНЯТТЯ 04» прибери # разом із пробілом одразу після нього й заміни habits та habit на свої назви:
@app.get("/api/habits")
def list_items():
return query("SELECT * FROM habit ORDER BY id DESC")Адресу ендпоінта називай за своєю таблицею: /api/games, /api/plants. Тут прийнято множину — за адресою повертається список.
Відступи в Python значущі: перед return має лишитися рівно чотири пробіли, і жодної табуляції. У заготовці рядок записаний як # return ..., тож якщо прибрати самий #, а пробіл після нього залишити, пробілів стане п’ять і Python видасть помилку.
Функція query вже підключена на початку файлу рядком from db import query, тому в main.py її викликають без префікса: query(...), а не db.query(...), як в init_db.py. Рядка import db у цьому файлі немає, і дописувати його не треба.
nano ~/app/project.yamlendpoints:
ping: "GET /api/ping"
about: "GET /api/about"
list: "GET /api/habits"Заповнюй рядок list — решту не чіпай, вони для наступних занять. Формат такий самий, як у сусідніх рядків: метод, пробіл, шлях. Зверни увагу, що це не готова адреса, яку можна відкрити: у рядку записані два значення підряд, і той, хто читає маніфест, бере з нього другу частину. Маніфест — єдине місце, де адреса твого списку зафіксована; за нею її знаходять і викладач, і скрипти перевірки.
systemctl --user restart club-api
systemctl --user is-active club-apiДруга команда друкує стан служби. Потрібне слово — active. Решта відповідей означає ось що:
activating — служба ще запускається. Зачекай дві-три секунди й виконай команду ще раз;inactive — служба зупинена. Запусти її: systemctl --user start club-api;failed — служба спробувала запуститися й завершилася з помилкою у твоєму коді. Дивись крок 13.Тепер запит із самого сервера. Кожна програма, яка приймає запити з мережі, слухає свій порт — номер, за яким система розрізняє такі програми на одній машині. Номер твоєї служби записаний у її файлі налаштувань, у рядку ExecStart після --port. Ця команда дістає його звідти й кладе у змінну PORT; змінна зберігається лише до виходу з ssh, тому після нового входу її задають знову:
PORT=$(grep -oP '(?<=--port )\d+' ~/.config/systemd/user/club-api.service)
echo "порт: $PORT"Якщо echo надрукував порт: і далі порожньо, номер не знайшовся. Тоді подивись файл очима — cat ~/.config/systemd/user/club-api.service — знайди в рядку ExecStart число після --port і підстав його в наступну команду замість $PORT.
curl -s http://127.0.0.1:$PORT/api/habitscurl — програма, яка робить запит за адресою й друкує відповідь просто в термінал: те саме, що робить браузер, тільки без оформлення. Адреса 127.0.0.1 означає «ця сама машина», тобто запит навіть не виходить у мережу. У відповідь має прийти масив JSON із твоїми п’ятьма записами в один довгий рядок.
journalctl --user -u club-api -n 30Читай знизу вгору й шукай перший рядок зі словом Error. Три помилки, які трапляються сьогодні найчастіше:
IndentationError — збилися відступи у функції ендпоінта;SyntaxError — забута дужка або лапка;NameError: name 'query' is not defined — випадково видалено рядок from db import query на початку файлу;NameError: name 'db' is not defined — у main.py написано db.query(...), як в init_db.py. У цьому файлі підключена сама функція, тому пиши query(...) без db.Виправ, збережи, знову systemctl --user restart club-api. Якщо служба кілька разів поспіль завершилася з помилкою, systemd припиняє спроби запуску: тоді потрібні дві команди — спочатку systemctl --user reset-failed club-api, яка скидає цей стан, потім restart.
curl -s http://91.219.61.4/u/ЛОГІН/api/habits | /opt/club/venv/bin/python -c 'import sys, json; print("елементів у відповіді:", len(json.load(sys.stdin)))'Тут curl забирає відповідь із публічної адреси й передає її Python, а той рахує довжину масиву. Це рівно те, що робить автоперевірка. Порівняй число з тим, яке дає база:
cd ~/app && /opt/club/venv/bin/python check_db.pycheck_db.py друкує два блоки. Порівнювати треба з першим його рядком — усього записів: це число має збігтися з числом після елементів у відповіді з попередньої команди. Список «за останні 7 днів» тут ні до чого, у ньому записів менше. Якщо числа не збігаються, причина майже завжди в запиті ендпоінта: зайвий LIMIT або зайва умова WHERE.
cd ~/app
git add -A
git status
git commit -m "Схема бази, перші записи і ендпоінт списку"
git push origin masterДва слова, які тут знадобляться. Коміт — збережений стан твоїх файлів із підписом, що саме змінилося; історія проєкту складається з таких станів. Репозиторій — сховище цієї історії; твоє лежить на сервері в теці ~/repo.git, і команда git push надсилає коміти саме туди. Слово master в останній команді — назва основної лінії історії в твоєму репозиторії. В інструкціях з інтернету на цьому місці часто стоїть main; у нас так не налаштовано, і з main команда відповість src refspec main does not match any.
Команда git status перед комітом показує, що саме потрапить в історію. У списку мають бути init_db.py, seed.py, check_db.py, main.py, project.yaml. Файлу бази там бути не повинно: він лежить у теці ~/data, а git стежить лише за тим, що всередині ~/app. Є й другий запобіжник — файл .gitignore: у ньому переліковано те, що git не бере в історію, навіть якщо воно лежить у потрібній теці. Рядок *.db там є ще із заняття 03. База змінюється щохвилини, і в історії змін коду їй не місце — для неї на занятті 11 буде окремий спосіб зберігання копій.
Відкрий http://91.219.61.4/k/<логін>/. Пункти сьогоднішнього заняття мають стати зарахованими протягом кількох хвилин після наступного прогону перевірки. Якщо якийсь пункт червоний, шукай його код у таблиці нижче.
Чотири пункти. Перший — обов’язковий мінімум заняття: без нього решта не рахується. Останній перевіряє викладач очима на розборі домашнього завдання.
| Код | Критерій | Що робить скрипт | Якщо не зараховано |
|---|---|---|---|
| L04.1 | первинний ключ і NOT NULL на обов’язкових полях | Бере з project.yaml назву головної таблиці, відкриває ~/data/app.db і перевіряє, що така таблиця в базі справді є. Первинний ключ і NOT NULL на полях зі списку main_table_required звіряє викладач — за виводом PRAGMA table_info з кроку 6. |
Спершу виконай крок 6 і подивись, що показує PRAGMA table_info — часто виявляється, що в main_table записана не та назва, що в базі. Якщо NOT NULL справді забули, найпростіше зараз, поки записів мало: видалити таблицю й створити заново (команди в розділі нижче). |
| L04.2 | у базі щонайменше 5 записів | Виконує SELECT COUNT(*) з головної таблиці й чекає число не менше п’яти. |
Допиши записи у seed.py і запусти його ще раз — але тільки з новими рядками, інакше попередні додадуться повторно. Перевір результат командою з кроку 8. |
| L04.3 | ендпоінт списку збігається з COUNT(*) головної таблиці | Робить запит ззовні на твою публічну адресу /u/<логін>/api/…, рахує кількість об’єктів у масиві JSON і незалежно рахує рядки в базі. Числа мають бути однакові — підставити своє число у відповідь не вийде. |
Перевір по черзі: чи заповнено endpoints.list; чи немає в запиті LIMIT або WHERE; чи та це таблиця, що вказана в main_table; чи перезапущено службу після зміни main.py. Якщо ендпоінт узагалі віддає 404 — адреса в маніфесті не збігається з адресою в декораторі @app.get. |
| L04.4 | записи справжні, а не «тест1» | Автоматика цього не перевіряє. На розборі домашнього завдання викладач вибірково відкриває кілька записів і читає їх. | Заміни вигадані рядки на справжні дані свого продукту. Видалити зайве можна командою DELETE з умовою за id. |
seed.py частинами: додав п’ять нових рядків у список — запустив — перевірив кількість командою з кроку 8. Якщо додав ті самі рядки вдруге, дивись розділ про дублікати.check_db.py ще один SELECT, який відповідає на осмислене питання про твої дані: скільки хвилин загалом за тиждень (SUM(minutes)), або п’ять найдовших записів, або всі записи однієї категорії. Запиши поруч у коментарі, на яке питання відповідає запит.~/app/README.md і додай розділ, у якому переліч свої стовпці: назва, тип, обов’язковий чи ні, що означає. Цей розділ читатиме людина, яка бачить проєкт уперше.http://91.219.61.4/u/<логін>/api/…. Ти маєш побачити текст JSON зі своїми записами.На початку наступного заняття буде питання «поясни рядок»: викладач покаже будь-який рядок із твого init_db.py або seed.py і попросить сказати, що він робить.
Python не зміг ані відкрити, ані створити файл бази. Найчастіша причина — немає теки ~/data: шлях до бази складається з неї, але сама вона не створюється. Перевір і, якщо теки немає, створи:
ls -ld ~/data
mkdir -p ~/dataПісля цього повтори крок 5. Друга можлива причина — у project.yaml у полі file написано не ім’я файлу, а шлях із похилими рисками. Там має стояти саме app.db.
База відкрилася, але таблиці в ній немає. Перевір по черзі три речі. Перше: чи запускався init_db.py — виконай його ще раз, він безпечний для повторного запуску. Друге: чи збігаються назви — у CREATE TABLE, у запиті й у полі main_table має стояти те саме слово, великі й малі літери теж мають збігатися. Третє: чи не з’явилася друга база. Ця команда покаже всі файли баз у твоїй домашній теці:
find ~ -name "*.db" -not -path "*/venv/*" -lsПравильний файл один — ~/data/app.db. Якщо знайшовся ще один файл бази, подивись у project.yaml поле file: там має бути тільки ім’я app.db, без тек і похилих рисок — решту шляху db.py додає сам. Виправ поле, видали зайвий файл і повтори крок 5.
seed.py запустився двічі, і кожен рядок додався повторно. Подивись, що саме в базі:
cd ~/app && /opt/club/venv/bin/python -c 'import db
for r in db.query("SELECT id, day, title FROM habit ORDER BY id"):
print(r)'Дублікати — це рядки з більшими номерами. Якщо перші п’ять записів правильні, а решта зайва, видали все, що після п’ятого:
cd ~/app && /opt/club/venv/bin/python -c 'import db; db.execute("DELETE FROM habit WHERE id > 5")'Команда DELETE без WHERE видаляє всю таблицю й нічого не перепитує, тому умову перечитай перед натисканням Enter.
Додати NOT NULL до наявного стовпця однією командою SQLite не дає. Поки записів п’ять, найпростіше створити таблицю заново. Спочатку виправ опис у init_db.py, потім видали стару таблицю й запусти обидва файли ще раз:
cd ~/app
/opt/club/venv/bin/python -c 'import db; c = db.connect(); c.execute("DROP TABLE habit"); c.commit()'
/opt/club/venv/bin/python init_db.py
/opt/club/venv/bin/python seed.pyDROP TABLE видаляє таблицю разом з усіма записами без можливості скасувати. Роби це лише зараз, поки даних мало; коли записів набереться 25, так уже не вийде, і таблицю доведеться переносити копіюванням.
Код 500 означає, що запит дійшов до твоєї функції і вона завершилася з помилкою. Дивись журнал: journalctl --user -u club-api -n 30. Найчастіші причини сьогодні — помилка в назві таблиці всередині SELECT (див. перший пункт) і неправильна назва стовпця в ORDER BY. Текст помилки називає стовпець прямо. Наприклад, no such column: data — це типова описка: стовпець із датою зветься day, а в запиті набрано data.
Файл бази є, але записувати в нього твоя програма не може. Перевір права — і на теку, і на файл:
ls -ld ~/data
ls -l ~/data/Власником і теки, і файлу має бути твій логін. Право на запис потрібне на обидва: під час запису SQLite створює поруч із базою тимчасовий файл app.db-journal, тому дозволу тільки на app.db не вистачить. Якщо власник правильний, а помилка лишається, дай собі права:
chmod u+rwx ~/data
chmod u+rw ~/data/app.dbЯкщо власник інший — сам ти цього не виправиш, скажи викладачеві.