diff --git a/.gitignore b/.gitignore index f963c95..123debb 100755 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,5 @@ __pycache__/ .env.* !.env.example -# Local bot archive +# Local application archives data/ diff --git a/README.md b/README.md index 74428f6..50c1790 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ Telegram-бот — только часть рабочего окружения ## Текущее состояние -Сейчас реализован Telegram-бот-секретарь версии `0.2.2`. Он сохраняет сообщения -рабочих тем, формирует Markdown-экспорты и отмечает завершённые обсуждения. +Сейчас реализованы Telegram-бот-секретарь версии `0.2.2` и первая backend-функция +версии `0.1.2`: возобновляемая выгрузка документов из ЦБД Минюста КР. | Компонент | Версия | Состояние | |---|---:|---| | Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния | -| Backend | — | ещё не создан | +| Backend | `0.1.2` | реализована выгрузка документов ЦБД Минюста КР | | Frontend | — | ещё не создан | -| Сбор и обработка правовых данных | — | ещё не созданы | +| Сбор и обработка правовых данных | `0.1.2` | реализован архиватор ЦБД Минюста КР | | RAG и база знаний | — | ещё не созданы | ## Структура репозитория @@ -30,6 +30,8 @@ docs/ структурированная документаци team/ материалы для команды tools/ telegram-bot/ бот рабочего Telegram-пространства +backend/ + ingestion/ получение и обновление правовых источников ``` Каталоги для загрузки и обработки источников, RAG, backend и frontend будут @@ -55,4 +57,4 @@ python3 -m unittest discover -s tools/telegram-bot -v --- -Акылдаш · Telegram-бот v0.2.2 · Backend — не создан · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..163afbe --- /dev/null +++ b/backend/README.md @@ -0,0 +1,64 @@ +# Backend Акылдаш + +Версия: `0.1.2` + +Первая backend-область проекта — загрузка правовых документов из официального +Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в +`ingestion/minjust_cbd.py`: его можно запускать как CLI сейчас и импортировать +из будущего планировщика backend без запуска отдельного процесса. + +## Пробная загрузка + +Из корня репозитория: + +```bash +python3 backend/ingestion/minjust_cbd.py --limit 10 +``` + +В интерактивном терминале отображаются процент, количество документов, +ошибки, скорость и примерное оставшееся время. + +Полная загрузка выполняется без `--limit`. По умолчанию архив сохраняется в +`data/minjust-cbd`, который исключён из Git. Повторный запуск пропускает уже +загруженные документы; `--refresh` принудительно проверяет их заново. +Если временный идентификатор списка API истечёт во время многодневной загрузки, +скрипт пересоздаст список на текущей странице и продолжит автоматически. + +В версии `0.1.0` обновление существующих документов выполняется полной проверкой +через `--refresh`. Инкрементальную проверку по `lastmod` из sitemap следует +добавить вместе с backend-планировщиком, когда будет определена частота запуска. + +## Защита API + +Запросы выполняются последовательно, по умолчанию не чаще одного в секунду. +Timeout одного ответа — 60 секунд, число попыток — 5, задержка между повторами +растёт экспоненциально. При HTTP 429 учитывается заголовок `Retry-After`. +Лимит действует на один процесс, поэтому одновременно следует запускать только +один экземпляр загрузчика. + +Более осторожный режим: + +```bash +python3 backend/ingestion/minjust_cbd.py --requests-per-second 0.5 +``` + +## Вызов из будущего backend + +```python +from ingestion.minjust_cbd import sync_archive + +result = sync_archive(output_path) +``` + +Планировщик, очередь задач и PostgreSQL пока не добавлены: модуль не зависит от +выбора будущего backend-фреймворка. + +## Проверка + +```bash +PYTHONPATH=backend python3 -m unittest backend/test_minjust_cbd.py -v +``` + +--- + +Акылдаш · Backend v0.1.2 · Frontend — не создан diff --git a/backend/ingestion/minjust_cbd.py b/backend/ingestion/minjust_cbd.py new file mode 100644 index 0000000..82899af --- /dev/null +++ b/backend/ingestion/minjust_cbd.py @@ -0,0 +1,373 @@ +#!/usr/bin/env python3 +"""Download and archive documents from the Kyrgyz Republic Ministry of Justice.""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import logging +import os +import sqlite3 +import sys +import tempfile +import time +import urllib.error +import urllib.parse +import urllib.request +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Callable, Iterable + +APP_VERSION = "0.1.2" +API_BASE_URL = "https://cbd.minjust.gov.kg/api/v1/OpenData/" +LANGUAGES = {"Rus": "ru", "Kyr": "ky"} +IMAGE_LANGUAGES = {"Russian": "ru", "Kyrgyz": "ky"} +LOGGER = logging.getLogger(__name__) + + +class CbdClient: + def __init__( + self, + requests_per_second: float = 1, + timeout: int = 60, + retries: int = 5, + ) -> None: + if requests_per_second <= 0: + raise ValueError("requests_per_second must be greater than zero") + if timeout <= 0 or retries <= 0: + raise ValueError("timeout and retries must be greater than zero") + # ponytail: per-process limit; add a distributed lock before multiple workers. + self.interval = 1 / requests_per_second + self.timeout = timeout + self.retries = retries + self.last_request = 0.0 + self.total_documents = 0 + + def request_json(self, method: str, parameters: Iterable[tuple[str, object]] = ()): + query = urllib.parse.urlencode(list(parameters), doseq=True) + extension = "" if method == "CheckAvailable" else ".json" + url = f"{API_BASE_URL}{method}{extension}" + (f"?{query}" if query else "") + for attempt in range(self.retries): + retry_delay = 2**attempt + wait = self.interval - (time.monotonic() - self.last_request) + if wait > 0: + time.sleep(wait) + request = urllib.request.Request( + url, + headers={ + "Accept": "application/json", + "User-Agent": f"Akyldash/{APP_VERSION} (+https://cbd.minjust.gov.kg)", + }, + ) + try: + self.last_request = time.monotonic() + with urllib.request.urlopen(request, timeout=self.timeout) as response: + body = response.read() + return json.loads(body) if body else None + except urllib.error.HTTPError as error: + if error.code != 429 and error.code < 500: + raise + if error.code == 429: + try: + retry_delay = max( + retry_delay, float(error.headers.get("Retry-After", 0)) + ) + except ValueError: + pass + except (TimeoutError, urllib.error.URLError): + pass + if attempt + 1 < self.retries: + time.sleep(retry_delay) + raise RuntimeError(f"Ministry of Justice API request failed: {url}") + + def check_available(self) -> None: + self.request_json("CheckAvailable") + + def document_codes(self, limit: int | None = None, page_size: int = 1000): + def query_page(page_number: int): + return self.request_json( + "GetDocumentListByQuery", + ( + ("Property", "Code"), + ("PageSize", page_size), + ("PageNumber", page_number), + ), + ) + + first = query_page(1) + yielded = 0 + page = first + page_number = 1 + self.total_documents = min(first["TotalCount"], limit or first["TotalCount"]) + while True: + for document in page["Documents"]: + yield document["Code"] + yielded += 1 + if limit is not None and yielded >= limit: + return + if yielded >= page["TotalCount"]: + return + page_number += 1 + try: + page = self.request_json( + "GetDocumentListById", + ( + ("DocumentListId", first["Id"]), + ("Property", "Code"), + ("PageSize", page_size), + ("PageNumber", page_number), + ), + ) + except urllib.error.HTTPError as error: + if error.code != 404: + raise + first = page = query_page(page_number) + self.total_documents = min( + first["TotalCount"], limit or first["TotalCount"] + ) + + def document(self, code: int) -> dict: + return self.request_json( + "GetDocument", + ( + ("Code", code), + ("Editions.Select", "all"), + ("Editions.Data", "all"), + ("Editions.Images.Select", "all"), + ("Editions.Images.Data", "all"), + ), + ) + + +@dataclass(frozen=True) +class SyncResult: + discovered: int = 0 + downloaded: int = 0 + skipped: int = 0 + failed: int = 0 + + +def format_duration(seconds: float) -> str: + seconds = max(0, round(seconds)) + hours, seconds = divmod(seconds, 3600) + minutes, seconds = divmod(seconds, 60) + return f"{hours:02d}:{minutes:02d}:{seconds:02d}" + + +def progress_line( + result: SyncResult, total: int, elapsed: float, width: int = 24 +) -> str: + processed = result.discovered + fraction = processed / total if total else 0 + filled = min(width, round(width * fraction)) + rate = processed / elapsed if elapsed > 0 else 0 + eta = (total - processed) / rate if rate else 0 + return ( + f"[{'#' * filled}{'-' * (width - filled)}] {fraction:6.2%} " + f"{processed}/{total} downloaded={result.downloaded} " + f"skipped={result.skipped} failed={result.failed} " + f"rate={rate:.2f}/s ETA={format_duration(eta)}" + ) + + +def terminal_progress() -> Callable[[SyncResult, int], None]: + started = time.monotonic() + + def update(result: SyncResult, total: int) -> None: + line = progress_line(result, total, time.monotonic() - started) + print( + f"\r{line}", + end="\n" if result.discovered >= total else "", + file=sys.stderr, + flush=True, + ) + + return update + + +def connect_manifest(path: Path) -> sqlite3.Connection: + path.parent.mkdir(parents=True, exist_ok=True) + connection = sqlite3.connect(path) + connection.execute( + """ + CREATE TABLE IF NOT EXISTS documents ( + code INTEGER PRIMARY KEY, + source_sha256 TEXT NOT NULL, + fetched_at TEXT NOT NULL + ) + """ + ) + connection.execute( + """ + CREATE TABLE IF NOT EXISTS errors ( + code INTEGER PRIMARY KEY, + message TEXT NOT NULL, + failed_at TEXT NOT NULL + ) + """ + ) + return connection + + +def atomic_write(path: Path, content: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as temporary: + temporary.write(content) + temporary_path = Path(temporary.name) + os.replace(temporary_path, path) + + +def json_bytes(value: object) -> bytes: + return (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode() + + +def archive_document(root: Path, document: dict) -> str: + code = int(document["Code"]) + source = json.dumps(document, ensure_ascii=False, sort_keys=True).encode() + source_sha256 = hashlib.sha256(source).hexdigest() + directory = root / "documents" / str(code) + editions = document.get("Editions") or [] + metadata = {key: value for key, value in document.items() if key != "Editions"} + atomic_write(directory / "metadata.json", json_bytes(metadata)) + + for edition in editions: + edition_directory = directory / "editions" / str(int(edition["Code"])) + edition_metadata = {key: value for key, value in edition.items() if key != "Data"} + edition_metadata["Images"] = [ + {key: value for key, value in image.items() if key != "Data"} + for image in edition.get("Images") or [] + ] + atomic_write(edition_directory / "metadata.json", json_bytes(edition_metadata)) + + for source_language, filename in LANGUAGES.items(): + html = (edition.get("Data") or {}).get(source_language) + if html: + atomic_write(edition_directory / f"{filename}.html", html.encode()) + + for image in edition.get("Images") or []: + if not image.get("Data"): + continue + language = IMAGE_LANGUAGES.get(image.get("Lang"), "unknown") + filename = Path(str(image.get("Name") or "image").replace("\\", "/")).name + if filename in {"", ".", ".."}: + raise ValueError(f"Invalid image filename in document {code}") + atomic_write( + edition_directory / "images" / language / filename, + base64.b64decode(image["Data"], validate=True), + ) + return source_sha256 + + +def sync_archive( + output: Path, + client: CbdClient | None = None, + limit: int | None = None, + refresh: bool = False, + progress: Callable[[SyncResult, int], None] | None = None, +) -> SyncResult: + client = client or CbdClient() + output.mkdir(parents=True, exist_ok=True) + connection = connect_manifest(output / "manifest.sqlite3") + known = {row[0] for row in connection.execute("SELECT code FROM documents")} + discovered = downloaded = skipped = failed = 0 + + client.check_available() + for code in client.document_codes(limit): + discovered += 1 + # ponytail: refresh scans all documents; use sitemap lastmod when scheduled + # update checks become frequent enough for the extra mapping logic to pay off. + if code in known and not refresh: + skipped += 1 + if progress: + progress( + SyncResult(discovered, downloaded, skipped, failed), + client.total_documents, + ) + continue + try: + document = client.document(code) + checksum = archive_document(output, document) + fetched_at = datetime.now(timezone.utc).isoformat() + with connection: + connection.execute( + """ + INSERT INTO documents (code, source_sha256, fetched_at) + VALUES (?, ?, ?) + ON CONFLICT(code) DO UPDATE SET + source_sha256 = excluded.source_sha256, + fetched_at = excluded.fetched_at + """, + (int(document["Code"]), checksum, fetched_at), + ) + connection.execute("DELETE FROM errors WHERE code = ?", (code,)) + downloaded += 1 + except Exception as error: # Continue the long-running archive after one bad record. + with connection: + connection.execute( + """ + INSERT INTO errors (code, message, failed_at) VALUES (?, ?, ?) + ON CONFLICT(code) DO UPDATE SET + message = excluded.message, + failed_at = excluded.failed_at + """, + (code, str(error), datetime.now(timezone.utc).isoformat()), + ) + failed += 1 + if progress: + progress( + SyncResult(discovered, downloaded, skipped, failed), + client.total_documents, + ) + if not progress and discovered % 100 == 0: + LOGGER.info( + "discovered=%s downloaded=%s skipped=%s failed=%s", + discovered, + downloaded, + skipped, + failed, + ) + connection.close() + return SyncResult(discovered, downloaded, skipped, failed) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, default=Path("data/minjust-cbd")) + parser.add_argument("--limit", type=int, help="download only the first N documents") + parser.add_argument("--refresh", action="store_true", help="redownload known documents") + parser.add_argument("--requests-per-second", type=float, default=1) + parser.add_argument("--timeout", type=int, default=60) + parser.add_argument("--retries", type=int, default=5) + parser.add_argument("--version", action="version", version=APP_VERSION) + return parser.parse_args() + + +def main() -> int: + arguments = parse_args() + if arguments.limit is not None and arguments.limit <= 0: + raise SystemExit("--limit must be greater than zero") + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") + result = sync_archive( + arguments.output, + CbdClient( + requests_per_second=arguments.requests_per_second, + timeout=arguments.timeout, + retries=arguments.retries, + ), + arguments.limit, + arguments.refresh, + terminal_progress() if sys.stderr.isatty() else None, + ) + print( + f"discovered={result.discovered} downloaded={result.downloaded} " + f"skipped={result.skipped} failed={result.failed}\n" + f"Akyldash Backend v{APP_VERSION} · Frontend — not created" + ) + return int(result.failed > 0) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backend/test_minjust_cbd.py b/backend/test_minjust_cbd.py new file mode 100644 index 0000000..e300858 --- /dev/null +++ b/backend/test_minjust_cbd.py @@ -0,0 +1,133 @@ +import base64 +import tempfile +import unittest +import urllib.error +from email.message import Message +from pathlib import Path +from unittest.mock import Mock, patch + +from ingestion.minjust_cbd import CbdClient, SyncResult, progress_line, sync_archive + + +class MinjustCbdTest(unittest.TestCase): + def test_archives_document_and_resumes(self): + image = base64.b64encode(b"image").decode() + document = { + "Code": 1, + "Name": {"Rus": "Закон", "Kyr": "Мыйзам"}, + "Editions": [ + { + "Code": 10, + "Name": {"Rus": "01.01.2026", "Kyr": "01.01.2026"}, + "Data": {"Rus": "

Закон

", "Kyr": "

Мыйзам

"}, + "Images": [ + { + "Lang": "Russian", + "Name": "image001.jpg", + "Data": image, + } + ], + } + ], + } + client = Mock() + client.total_documents = 1 + client.document_codes.return_value = [1] + client.document.return_value = document + + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) + first = sync_archive(output, client) + second = sync_archive(output, client) + refreshed = sync_archive(output, client, refresh=True) + + self.assertEqual(first.downloaded, 1) + self.assertEqual(second.skipped, 1) + self.assertEqual(refreshed.downloaded, 1) + self.assertEqual(client.document.call_count, 2) + self.assertEqual( + (output / "documents/1/editions/10/ru.html").read_text(), + "

Закон

", + ) + self.assertEqual( + (output / "documents/1/editions/10/images/ru/image001.jpg").read_bytes(), + b"image", + ) + + def test_uses_stable_list_id_for_next_page(self): + client = CbdClient(requests_per_second=1000) + client.request_json = Mock( + side_effect=[ + { + "Id": "list-id", + "TotalCount": 3, + "Documents": [{"Code": 1}, {"Code": 2}], + }, + { + "Id": "list-id", + "TotalCount": 3, + "Documents": [{"Code": 3}], + }, + ] + ) + + self.assertEqual(list(client.document_codes(page_size=2)), [1, 2, 3]) + self.assertEqual( + client.request_json.call_args_list[1].args[0], "GetDocumentListById" + ) + + def test_recreates_expired_list_at_current_page(self): + client = CbdClient(requests_per_second=1000) + client.request_json = Mock( + side_effect=[ + { + "Id": "expired-list", + "TotalCount": 3, + "Documents": [{"Code": 1}, {"Code": 2}], + }, + urllib.error.HTTPError("https://example.test", 404, "", {}, None), + { + "Id": "new-list", + "TotalCount": 3, + "Documents": [{"Code": 3}], + }, + ] + ) + + self.assertEqual(list(client.document_codes(page_size=2)), [1, 2, 3]) + recovery_call = client.request_json.call_args_list[2] + self.assertEqual(recovery_call.args[0], "GetDocumentListByQuery") + self.assertIn(("PageNumber", 2), recovery_call.args[1]) + + def test_formats_progress_with_rate_and_eta(self): + line = progress_line( + SyncResult(discovered=50, downloaded=48, skipped=1, failed=1), + total=100, + elapsed=25, + ) + + self.assertIn("50.00%", line) + self.assertIn("rate=2.00/s", line) + self.assertIn("ETA=00:00:25", line) + + @patch("ingestion.minjust_cbd.time.sleep") + @patch("ingestion.minjust_cbd.urllib.request.urlopen") + def test_respects_retry_after_on_rate_limit(self, urlopen, sleep): + headers = Message() + headers["Retry-After"] = "7" + response = Mock() + response.read.return_value = b"{}" + response.__enter__ = Mock(return_value=response) + response.__exit__ = Mock(return_value=False) + urlopen.side_effect = [ + urllib.error.HTTPError("https://example.test", 429, "", headers, None), + response, + ] + + CbdClient(requests_per_second=1000, retries=2).request_json("Example") + + self.assertIn(((7.0,), {}), [(call.args, call.kwargs) for call in sleep.call_args_list]) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/README.md b/docs/README.md index e279142..424e6a7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,6 +17,11 @@ - [План Telegram-пространства](operations/telegram-workspace-plan.md) — темы, роли и этапы развития командного окружения. +## Backend + +- [Выгрузка ЦБД Минюста КР](../backend/README.md) — запуск, хранение и проверка + загрузчика правовых документов. + ## Команда - [Навыки работы с ИИ](team/ai-skills-for-beginners.md) — короткие практики для @@ -24,4 +29,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend — не создан · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан diff --git a/docs/decisions/001-telegram-workspace-mvp.md b/docs/decisions/001-telegram-workspace-mvp.md index a7de588..ac5e127 100644 --- a/docs/decisions/001-telegram-workspace-mvp.md +++ b/docs/decisions/001-telegram-workspace-mvp.md @@ -78,4 +78,4 @@ Telegram позволяет запретить пользователям отп --- -Акылдаш · Telegram-бот v0.2.2 +Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан diff --git a/docs/operations/project-status.md b/docs/operations/project-status.md index 620f958..35aaddf 100644 --- a/docs/operations/project-status.md +++ b/docs/operations/project-status.md @@ -1,18 +1,18 @@ # Статус проекта -Последняя проверка: 2026-08-03 +Последняя проверка: 2026-08-06 Назначение документа: быстро восстановить контекст проекта для участников команды и будущих агентов. - Telegram-бот: `0.2.2` - Telegram-бот на Synology: `0.2.1` -- Backend: не создан +- Backend: `0.1.2` - Frontend: не создан ## Краткий итог -Репозиторий переориентирован с отдельного бота на весь проект юридической информационно-аналитической платформы. Telegram-бот выделен в инструмент рабочего окружения: он сохраняет сообщения разрешённых тем в SQLite, экспортирует обсуждения в Markdown и постоянно запущен в Container Manager на Synology. Версия `0.2.2` будет развёрнута после слияния изменений; сейчас на Synology работает `0.2.1`. +Репозиторий переориентирован с отдельного бота на весь проект юридической информационно-аналитической платформы. Telegram-бот выделен в инструмент рабочего окружения. Создана первая backend-функция: возобновляемая выгрузка документов из официального Open Data API ЦБД Минюста КР. -Ближайшая цель — 2026-08-04 зафиксировать MVP с командой, настроить резервное копирование SQLite и затем проверить полный рабочий цикл на реальном обсуждении. +Ближайшая цель — расширить пилотную выборку ЦБД, затем добавить инкрементальную проверку sitemap при создании backend-планировщика. ## Уже сделано @@ -67,6 +67,9 @@ - Локальный Git-репозиторий восстановлен и привязан к Gitea. - Репозиторий организован как основа всего проекта, а не отдельного бота. - Бот развёрнут в Container Manager на Synology; автозапуск после перезапуска менеджера проверен. +- Реализован backend-загрузчик ЦБД Минюста КР версии `0.1.2` без внешних зависимостей. +- Загрузчик сохраняет метаданные, редакции RU/KY и изображения, а прогресс — в SQLite. +- Пилотная выгрузка двух документов и возобновление без повторного скачивания проверены на живом API. ### Развёртывание @@ -125,6 +128,19 @@ ## История изменений статуса +### 2026-08-06 + +- Добавлен терминальный прогрессбар со скоростью и расчётным временем завершения. +- Обработка HTTP 429 учитывает рекомендованную сервером задержку `Retry-After`. +- Добавлено автоматическое восстановление после истечения серверного списка документов. +- Версия backend обновлена до `0.1.2`. + +### 2026-08-05 + +- Создана область `backend/ingestion` для получения правовых источников. +- Реализована возобновляемая выгрузка ЦБД Минюста КР версии `0.1.0`. +- Проверены двуязычные редакции, изображения, SQLite-манифест и повторный запуск. + ### 2026-08-03 - Репозиторий перестроен под весь проект юридической платформы; Telegram-бот перенесён в `tools/telegram-bot`. @@ -162,4 +178,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend — не создан · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан diff --git a/docs/operations/telegram-workspace-plan.md b/docs/operations/telegram-workspace-plan.md index d6d1969..17a2287 100644 --- a/docs/operations/telegram-workspace-plan.md +++ b/docs/operations/telegram-workspace-plan.md @@ -398,4 +398,4 @@ Git сохраняет актуальную версию --- -Акылдаш · Telegram-бот v0.2.2 +Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан diff --git a/docs/product/project-overview.md b/docs/product/project-overview.md index 18d5ff7..40c4726 100644 --- a/docs/product/project-overview.md +++ b/docs/product/project-overview.md @@ -44,4 +44,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend — не создан · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан diff --git a/docs/team/ai-skills-for-beginners.md b/docs/team/ai-skills-for-beginners.md index 9cdb473..732708f 100644 --- a/docs/team/ai-skills-for-beginners.md +++ b/docs/team/ai-skills-for-beginners.md @@ -180,4 +180,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 +Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан diff --git a/tools/telegram-bot/README.md b/tools/telegram-bot/README.md index b8918af..042caff 100644 --- a/tools/telegram-bot/README.md +++ b/tools/telegram-bot/README.md @@ -48,4 +48,4 @@ python3 -m unittest -v --- -Акылдаш · Telegram-бот v0.2.2 +Акылдаш · Telegram-бот v0.2.2 · Backend v0.1.2 · Frontend — не создан diff --git a/План_выгрузки_документов_ЦБД_Минюста_КР_v0.1.0.pdf b/План_выгрузки_документов_ЦБД_Минюста_КР_v0.1.0.pdf new file mode 100644 index 0000000..6dacbb5 Binary files /dev/null and b/План_выгрузки_документов_ЦБД_Минюста_КР_v0.1.0.pdf differ