refactor: пакеты core и modules.jira — перенос без логики (#87)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
808d7b710c
commit
6d7aebe01c
@ -1,87 +1 @@
|
|||||||
"""Команды jira-hub: init (настройки из config.example.json), serve (HTTP), import (CSV-пакет), mcp (stdio), tokens,
|
from .core.cli import main
|
||||||
agent publish (версия агента для самообновления)."""
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from . import db, importer, paths, projects, settings
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None) -> int:
|
|
||||||
ap = argparse.ArgumentParser(prog="jira-hub", description="Мост Jira для АРМ КА: хаб")
|
|
||||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
||||||
p = sub.add_parser("init", help="перенести настройки проекта из config.example.json, создать токены")
|
|
||||||
p.add_argument("--config", required=True)
|
|
||||||
p.add_argument("--force", action="store_true")
|
|
||||||
p = sub.add_parser("serve", help="запустить HTTP API")
|
|
||||||
p.add_argument("--host", default="0.0.0.0")
|
|
||||||
p.add_argument("--port", type=int)
|
|
||||||
p = sub.add_parser("import", help="импорт CSV-пакета admin/jira-sync в заметки")
|
|
||||||
p.add_argument("folder")
|
|
||||||
p = sub.add_parser("mcp", help="MCP-сервер по stdio")
|
|
||||||
p.add_argument("--project", help="метка проекта, к которому привязана сессия")
|
|
||||||
sub.add_parser("tokens", help="показать токены агента и клиента")
|
|
||||||
p = sub.add_parser("agent", help="версия агента для самообновления")
|
|
||||||
agent_sub = p.add_subparsers(dest="agent_cmd", required=True)
|
|
||||||
p = agent_sub.add_parser("publish", help="опубликовать agent.py (версия только вверх)")
|
|
||||||
p.add_argument("path")
|
|
||||||
args = ap.parse_args(argv)
|
|
||||||
|
|
||||||
if args.cmd == "agent":
|
|
||||||
from . import release
|
|
||||||
try:
|
|
||||||
m = release.publish(paths.release_dir(), args.path)
|
|
||||||
except release.PublishError as e:
|
|
||||||
print(e, file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
print(f"опубликована v{m['version']}, sha256 {m['sha256'][:12]}…, {m['size']} байт")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if args.cmd == "serve":
|
|
||||||
from . import api
|
|
||||||
hub = api.Hub(paths.db_path())
|
|
||||||
conn = hub.conn()
|
|
||||||
port = args.port or int(settings.get(conn, "port") or 8790)
|
|
||||||
conn.close()
|
|
||||||
api.serve(hub, args.host, port)
|
|
||||||
return 0
|
|
||||||
if args.cmd == "mcp":
|
|
||||||
from . import mcp_server
|
|
||||||
mcp_server.run(args.project)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
conn = db.connect(paths.db_path())
|
|
||||||
settings.ensure_defaults(conn)
|
|
||||||
try:
|
|
||||||
if args.cmd == "init":
|
|
||||||
with open(args.config, encoding="utf-8") as f:
|
|
||||||
cfg = json.load(f)
|
|
||||||
try:
|
|
||||||
keys = settings.init_from_config(conn, cfg, force=args.force)
|
|
||||||
except RuntimeError as e:
|
|
||||||
print(e, file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
print("перенесено: " + ", ".join(keys))
|
|
||||||
if projects.ensure_legacy(conn, cfg.get("sprints", {}).get("board_id")):
|
|
||||||
board = projects.get(conn, projects.LEGACY_LABEL)["board_id"]
|
|
||||||
print(f"проект {projects.LEGACY_LABEL} «{projects.LEGACY_NAME}», доска {board}")
|
|
||||||
_print_tokens(conn)
|
|
||||||
return 0
|
|
||||||
if args.cmd == "import":
|
|
||||||
print(json.dumps(importer.import_package(conn, args.folder), ensure_ascii=False, indent=1))
|
|
||||||
return 0
|
|
||||||
if args.cmd == "tokens":
|
|
||||||
_print_tokens(conn)
|
|
||||||
return 0
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
return 2
|
|
||||||
|
|
||||||
|
|
||||||
def _print_tokens(conn) -> None:
|
|
||||||
tok = settings.tokens(conn)
|
|
||||||
print(f"агентский токен: {tok['agent']}\nклиентский токен: {tok['client']}\nбаза: {paths.db_path()}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
|
|||||||
0
src/jira_hub/core/__init__.py
Normal file
0
src/jira_hub/core/__init__.py
Normal file
@ -15,8 +15,9 @@ from pathlib import Path
|
|||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from urllib.parse import parse_qs, quote, unquote, urlparse
|
from urllib.parse import parse_qs, quote, unquote, urlparse
|
||||||
|
|
||||||
from . import attachments, backup, db, files, history, mirror, projects, queue, relay, release, settings
|
from . import backup, db, files, projects, queue, relay, release, settings
|
||||||
from .commands import CommandError
|
from ..modules.jira import attachments, history, mirror
|
||||||
|
from ..modules.jira.commands import CommandError
|
||||||
from .paths import backups_dir as default_backups_dir
|
from .paths import backups_dir as default_backups_dir
|
||||||
from .paths import secure_dir
|
from .paths import secure_dir
|
||||||
from .queue import ClaimedError
|
from .queue import ClaimedError
|
||||||
@ -24,7 +25,7 @@ from .queue import ClaimedError
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_BODY = 16 * 1024 * 1024 # предел тела запроса, байт
|
MAX_BODY = 16 * 1024 * 1024 # предел тела запроса, байт
|
||||||
WEB_APP = Path(__file__).parent / "web" / "app" # сборка веб-приложения (make web, не в git); читается на каждый запрос
|
WEB_APP = Path(__file__).resolve().parent.parent / "web" / "app" # сборка веб-приложения (make web, не в git); читается на каждый запрос
|
||||||
ASSET_TYPES = {".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml",
|
ASSET_TYPES = {".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml",
|
||||||
".png": "image/png", ".ico": "image/x-icon", ".woff2": "font/woff2", ".map": "application/json"}
|
".png": "image/png", ".ico": "image/x-icon", ".woff2": "font/woff2", ".map": "application/json"}
|
||||||
ASSET_CACHE = {"Cache-Control": "public, max-age=31536000, immutable"} # имена файлов сборки содержат хэш (R-3)
|
ASSET_CACHE = {"Cache-Control": "public, max-age=31536000, immutable"} # имена файлов сборки содержат хэш (R-3)
|
||||||
88
src/jira_hub/core/cli.py
Normal file
88
src/jira_hub/core/cli.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
"""Команды jira-hub: init (настройки из config.example.json), serve (HTTP), import (CSV-пакет), mcp (stdio), tokens,
|
||||||
|
agent publish (версия агента для самообновления)."""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import db, paths, projects, settings
|
||||||
|
from ..modules.jira import importer
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
ap = argparse.ArgumentParser(prog="jira-hub", description="Мост Jira для АРМ КА: хаб")
|
||||||
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||||
|
p = sub.add_parser("init", help="перенести настройки проекта из config.example.json, создать токены")
|
||||||
|
p.add_argument("--config", required=True)
|
||||||
|
p.add_argument("--force", action="store_true")
|
||||||
|
p = sub.add_parser("serve", help="запустить HTTP API")
|
||||||
|
p.add_argument("--host", default="0.0.0.0")
|
||||||
|
p.add_argument("--port", type=int)
|
||||||
|
p = sub.add_parser("import", help="импорт CSV-пакета admin/jira-sync в заметки")
|
||||||
|
p.add_argument("folder")
|
||||||
|
p = sub.add_parser("mcp", help="MCP-сервер по stdio")
|
||||||
|
p.add_argument("--project", help="метка проекта, к которому привязана сессия")
|
||||||
|
sub.add_parser("tokens", help="показать токены агента и клиента")
|
||||||
|
p = sub.add_parser("agent", help="версия агента для самообновления")
|
||||||
|
agent_sub = p.add_subparsers(dest="agent_cmd", required=True)
|
||||||
|
p = agent_sub.add_parser("publish", help="опубликовать agent.py (версия только вверх)")
|
||||||
|
p.add_argument("path")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
if args.cmd == "agent":
|
||||||
|
from . import release
|
||||||
|
try:
|
||||||
|
m = release.publish(paths.release_dir(), args.path)
|
||||||
|
except release.PublishError as e:
|
||||||
|
print(e, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"опубликована v{m['version']}, sha256 {m['sha256'][:12]}…, {m['size']} байт")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.cmd == "serve":
|
||||||
|
from . import api
|
||||||
|
hub = api.Hub(paths.db_path())
|
||||||
|
conn = hub.conn()
|
||||||
|
port = args.port or int(settings.get(conn, "port") or 8790)
|
||||||
|
conn.close()
|
||||||
|
api.serve(hub, args.host, port)
|
||||||
|
return 0
|
||||||
|
if args.cmd == "mcp":
|
||||||
|
from . import mcp_server
|
||||||
|
mcp_server.run(args.project)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
conn = db.connect(paths.db_path())
|
||||||
|
settings.ensure_defaults(conn)
|
||||||
|
try:
|
||||||
|
if args.cmd == "init":
|
||||||
|
with open(args.config, encoding="utf-8") as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
try:
|
||||||
|
keys = settings.init_from_config(conn, cfg, force=args.force)
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(e, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print("перенесено: " + ", ".join(keys))
|
||||||
|
if projects.ensure_legacy(conn, cfg.get("sprints", {}).get("board_id")):
|
||||||
|
board = projects.get(conn, projects.LEGACY_LABEL)["board_id"]
|
||||||
|
print(f"проект {projects.LEGACY_LABEL} «{projects.LEGACY_NAME}», доска {board}")
|
||||||
|
_print_tokens(conn)
|
||||||
|
return 0
|
||||||
|
if args.cmd == "import":
|
||||||
|
print(json.dumps(importer.import_package(conn, args.folder), ensure_ascii=False, indent=1))
|
||||||
|
return 0
|
||||||
|
if args.cmd == "tokens":
|
||||||
|
_print_tokens(conn)
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def _print_tokens(conn) -> None:
|
||||||
|
tok = settings.tokens(conn)
|
||||||
|
print(f"агентский токен: {tok['agent']}\nклиентский токен: {tok['client']}\nбаза: {paths.db_path()}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@ -15,9 +15,9 @@ from pathlib import Path
|
|||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
from . import paths
|
from . import paths
|
||||||
from .attachments import fmt_size
|
from ..modules.jira.attachments import fmt_size
|
||||||
from .client import HubClient, HubError, HubUnavailable
|
from .client import HubClient, HubError, HubUnavailable
|
||||||
from .commands import describe, is_temp
|
from ..modules.jira.commands import describe, is_temp
|
||||||
from .files import MAX_SIZE
|
from .files import MAX_SIZE
|
||||||
|
|
||||||
OFFLINE = "хаб не запущен: jira-hub serve"
|
OFFLINE = "хаб не запущен: jira-hub serve"
|
||||||
@ -25,7 +25,7 @@ POLL_SECONDS = 5
|
|||||||
AUTHOR = "claude"
|
AUTHOR = "claude"
|
||||||
BOTH_FILE_AND_PATH = "у ensure_attachment либо path, либо file"
|
BOTH_FILE_AND_PATH = "у ensure_attachment либо path, либо file"
|
||||||
NOT_BOUND = "сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)"
|
NOT_BOUND = "сессия не привязана к проекту: запустите MCP с --project <метка> (список — projects)"
|
||||||
HUB_ROOT = Path(__file__).resolve().parents[2] # корень репозитория хаба, из которого загружен пакет (решение 19)
|
HUB_ROOT = Path(__file__).resolve().parents[3] # корень репозитория хаба, из которого загружен пакет (решение 19)
|
||||||
PROJECT: str | None = None # привязка сессии, задаётся run(project)
|
PROJECT: str | None = None # привязка сессии, задаётся run(project)
|
||||||
mcp = FastMCP("jira-hub")
|
mcp = FastMCP("jira-hub")
|
||||||
|
|
||||||
@ -6,8 +6,9 @@ import logging
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from . import backup as _backup # noqa: F401 (имя модуля занято параметром backup в claim)
|
from . import backup as _backup # noqa: F401 (имя модуля занято параметром backup в claim)
|
||||||
from . import commands as C
|
from ..modules.jira import commands as C
|
||||||
from . import attachments, db, files, history, mirror, projects, settings
|
from . import db, files, projects, settings
|
||||||
|
from ..modules.jira import attachments, history, mirror
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
0
src/jira_hub/modules/__init__.py
Normal file
0
src/jira_hub/modules/__init__.py
Normal file
0
src/jira_hub/modules/jira/__init__.py
Normal file
0
src/jira_hub/modules/jira/__init__.py
Normal file
@ -3,7 +3,7 @@
|
|||||||
базу и хранилище файлов, очередь и HTTP не знает."""
|
базу и хранилище файлов, очередь и HTTP не знает."""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from . import files
|
from ...core import files
|
||||||
|
|
||||||
# lookahead: совпадения перекрываются, поэтому одиночный «!» ранее в тексте не съедает открывающий «!» настоящей ссылки;
|
# lookahead: совпадения перекрываются, поэтому одиночный «!» ранее в тексте не съедает открывающий «!» настоящей ссылки;
|
||||||
# после открывающего «!» пробела нет (как в Jira), иначе закрывающий «!» одной картинки породил бы ложное имя
|
# после открывающего «!» пробела нет (как в Jira), иначе закрывающий «!» одной картинки породил бы ложное имя
|
||||||
@ -2,7 +2,7 @@
|
|||||||
refresh. Пишет таблицу только приём журнала (POST /agent/history); API и MCP её только читают (spec data-model)."""
|
refresh. Пишет таблицу только приём журнала (POST /agent/history); API и MCP её только читают (spec data-model)."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from . import db, projects, settings
|
from ...core import db, projects, settings
|
||||||
|
|
||||||
SINCE_MARGIN = timedelta(minutes=15) # расхождение часов Mac mini и Jira и длительность прогона (design #5)
|
SINCE_MARGIN = timedelta(minutes=15) # расхождение часов Mac mini и Jira и длительность прогона (design #5)
|
||||||
|
|
||||||
@ -4,7 +4,8 @@
|
|||||||
import csv
|
import csv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from . import db, mirror, settings
|
from . import mirror
|
||||||
|
from ...core import db, settings
|
||||||
|
|
||||||
|
|
||||||
def _rows(path: Path) -> list[dict]:
|
def _rows(path: Path) -> list[dict]:
|
||||||
@ -2,7 +2,8 @@
|
|||||||
Зеркало пишет только загрузка состояния от агента; заметки обновление зеркала не трогает."""
|
Зеркало пишет только загрузка состояния от агента; заметки обновление зеркала не трогает."""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from . import attachments, db, history
|
from . import attachments, history
|
||||||
|
from ...core import db
|
||||||
|
|
||||||
ISSUE_COLUMNS = ["key", "id", "issue_type", "summary", "description", "status", "priority", "assignee", "reporter",
|
ISSUE_COLUMNS = ["key", "id", "issue_type", "summary", "description", "status", "priority", "assignee", "reporter",
|
||||||
"duedate", "planned_start", "planned_end", "epic_key", "labels", "sprint", "created", "jira_updated"]
|
"duedate", "planned_start", "planned_end", "epic_key", "labels", "sprint", "created", "jira_updated"]
|
||||||
@ -2,7 +2,7 @@
|
|||||||
ResourceWarning «unclosed database» (hub-hygiene-d, решение 14; M-9/O-4 #4)."""
|
ResourceWarning «unclosed database» (hub-hygiene-d, решение 14; M-9/O-4 #4)."""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import db, projects
|
from jira_hub.core import db, projects
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@ -11,9 +11,9 @@ from datetime import timedelta
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import api, db, files, projects, queue, release, settings
|
from jira_hub.core import api, db, files, projects, queue, release, settings
|
||||||
from jira_hub.client import HubClient, HubError
|
from jira_hub.core.client import HubClient, HubError
|
||||||
from jira_hub.commands import CommandError
|
from jira_hub.modules.jira.commands import CommandError
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -157,7 +157,7 @@ def test_backup_created_on_first_write(hub, tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_client_unavailable():
|
def test_client_unavailable():
|
||||||
from jira_hub.client import HubUnavailable
|
from jira_hub.core.client import HubUnavailable
|
||||||
with pytest.raises(HubUnavailable):
|
with pytest.raises(HubUnavailable):
|
||||||
HubClient("http://127.0.0.1:9", "t", timeout=1).get("/issues")
|
HubClient("http://127.0.0.1:9", "t", timeout=1).get("/issues")
|
||||||
|
|
||||||
@ -1040,7 +1040,7 @@ ATT_NAME = "скрин 1.png"
|
|||||||
def _seed_attachment(hub, mime="image/png", size=3, name=ATT_NAME):
|
def _seed_attachment(hub, mime="image/png", size=3, name=ATT_NAME):
|
||||||
conn = hub["hub"].conn()
|
conn = hub["hub"].conn()
|
||||||
try:
|
try:
|
||||||
from jira_hub import mirror
|
from jira_hub.modules.jira import mirror
|
||||||
mirror.apply_state(conn, [{"key": "A-1", "summary": "т", "labels": [], "attachments": [
|
mirror.apply_state(conn, [{"key": "A-1", "summary": "т", "labels": [], "attachments": [
|
||||||
{"id": "31337", "filename": name, "size": size, "mime_type": mime, "created": "2026-10-20T10:00:00.000+0300"}]}], [])
|
{"id": "31337", "filename": name, "size": size, "mime_type": mime, "created": "2026-10-20T10:00:00.000+0300"}]}], [])
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import json
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import attachments, db, files, mirror
|
from jira_hub.core import db, files
|
||||||
|
from jira_hub.modules.jira import attachments, mirror
|
||||||
|
|
||||||
|
|
||||||
def test_fmt_size():
|
def test_fmt_size():
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from jira_hub import api, db, mcp_server, settings
|
from jira_hub.core import api, db, mcp_server, settings
|
||||||
|
|
||||||
FIXTURES = Path(__file__).parent / "fixtures" / "baseline"
|
FIXTURES = Path(__file__).parent / "fixtures" / "baseline"
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from jira_hub import cli, paths, projects, release, settings
|
from jira_hub.core import cli, paths, projects, release, settings
|
||||||
|
|
||||||
FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json"
|
FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json"
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ def test_agent_publish_write_failure(tmp_path, monkeypatch, capsys):
|
|||||||
|
|
||||||
|
|
||||||
def test_mcp_project_option(monkeypatch):
|
def test_mcp_project_option(monkeypatch):
|
||||||
from jira_hub import mcp_server
|
from jira_hub.core import mcp_server
|
||||||
seen = []
|
seen = []
|
||||||
monkeypatch.setattr(mcp_server, "run", lambda project=None: seen.append(project))
|
monkeypatch.setattr(mcp_server, "run", lambda project=None: seen.append(project))
|
||||||
assert cli.main(["mcp", "--project", "AWPCA"]) == 0
|
assert cli.main(["mcp", "--project", "AWPCA"]) == 0
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import commands, mirror
|
from jira_hub.modules.jira import commands, mirror
|
||||||
from jira_hub.commands import CommandError
|
from jira_hub.modules.jira.commands import CommandError
|
||||||
|
|
||||||
NOW = "strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now')"
|
NOW = "strftime('%Y-%m-%dT%H:%M:%S.000+0300', 'now')"
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import threading
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import backup, db, paths, projects, queue, settings
|
from jira_hub.core import backup, db, paths, projects, queue, settings
|
||||||
|
|
||||||
|
|
||||||
def test_connect_creates_schema(open_db):
|
def test_connect_creates_schema(open_db):
|
||||||
@ -154,7 +154,7 @@ def test_data_dir_is_private(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_existing_backups_dir_made_private_on_start(tmp_path):
|
def test_existing_backups_dir_made_private_on_start(tmp_path):
|
||||||
from jira_hub import api
|
from jira_hub.core import api
|
||||||
backups = tmp_path / "backups"
|
backups = tmp_path / "backups"
|
||||||
backups.mkdir()
|
backups.mkdir()
|
||||||
backups.chmod(0o755)
|
backups.chmod(0o755)
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import threading
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import api, settings
|
from jira_hub.core import api, settings
|
||||||
from jira_hub.client import HubClient, HubError
|
from jira_hub.core.client import HubClient, HubError
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import time
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import db, files, settings
|
from jira_hub.core import db, files, settings
|
||||||
|
|
||||||
ABC = hashlib.sha256(b"abc").hexdigest() # ba7816bf…
|
ABC = hashlib.sha256(b"abc").hexdigest() # ba7816bf…
|
||||||
DAY = 86400
|
DAY = 86400
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import history
|
from jira_hub.modules.jira import history
|
||||||
|
|
||||||
ITEM = {"history_id": "5001", "seq": 0, "key": "X", "created": "2026-10-22T09:00:00.000+0300", "author": "Петров Пётр",
|
ITEM = {"history_id": "5001", "seq": 0, "key": "X", "created": "2026-10-22T09:00:00.000+0300", "author": "Петров Пётр",
|
||||||
"field_id": "status", "field_name": "status", "from": "3", "from_string": "В работе", "to": "10001",
|
"field_id": "status", "field_name": "status", "from": "3", "from_string": "В работе", "to": "10001",
|
||||||
@ -29,7 +29,7 @@ def test_record_keeps_first_version(conn):
|
|||||||
|
|
||||||
|
|
||||||
def test_record_uses_field_settings(conn):
|
def test_record_uses_field_settings(conn):
|
||||||
from jira_hub import settings
|
from jira_hub.core import settings
|
||||||
settings.set_(conn, "field_ids", {"planned_end": "customfield_10201"})
|
settings.set_(conn, "field_ids", {"planned_end": "customfield_10201"})
|
||||||
history.record_changes(conn, [{**ITEM, "field_id": "customfield_10201", "field_name": "Плановое окончание"}])
|
history.record_changes(conn, [{**ITEM, "field_id": "customfield_10201", "field_name": "Плановое окончание"}])
|
||||||
assert conn.execute("SELECT field FROM issue_change").fetchone()["field"] == "planned_end"
|
assert conn.execute("SELECT field FROM issue_change").fetchone()["field"] == "planned_end"
|
||||||
@ -92,7 +92,7 @@ def test_feed_closed_in_two_hours(conn): # сце
|
|||||||
|
|
||||||
|
|
||||||
def test_feed_comment(conn): # «Комментарий в ленте»
|
def test_feed_comment(conn): # «Комментарий в ленте»
|
||||||
from jira_hub import mirror
|
from jira_hub.modules.jira import mirror
|
||||||
mirror.apply_state(conn, [{"key": "X", "labels": ["AWPCA"], "comments": [
|
mirror.apply_state(conn, [{"key": "X", "labels": ["AWPCA"], "comments": [
|
||||||
{"id": "7", "author": "Иванов", "body": "Готово к проверке", "created": "2026-10-22T13:30:00.000+0300"}]}], [])
|
{"id": "7", "author": "Иванов", "body": "Готово к проверке", "created": "2026-10-22T13:30:00.000+0300"}]}], [])
|
||||||
got = history.feed(conn, "2026-10-22T10:00:00+00:00", "2026-10-22T12:00:00+00:00", history.DEFAULT_KINDS)
|
got = history.feed(conn, "2026-10-22T10:00:00+00:00", "2026-10-22T12:00:00+00:00", history.DEFAULT_KINDS)
|
||||||
@ -163,7 +163,7 @@ def test_parse_kinds_unknown():
|
|||||||
|
|
||||||
|
|
||||||
def test_issue_changes(conn): # «Задача с историей»
|
def test_issue_changes(conn): # «Задача с историей»
|
||||||
from jira_hub import mirror
|
from jira_hub.modules.jira import mirror
|
||||||
_issue(conn, "X", ["AWPCA"])
|
_issue(conn, "X", ["AWPCA"])
|
||||||
_change(conn, "X", "1", "2026-10-21T11:00:00Z", "status", "a", "b")
|
_change(conn, "X", "1", "2026-10-21T11:00:00Z", "status", "a", "b")
|
||||||
_change(conn, "X", "2", "2026-10-22T11:00:00Z", "description", "c", "d", field_id="description")
|
_change(conn, "X", "2", "2026-10-22T11:00:00Z", "description", "c", "d", field_id="description")
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import csv
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import importer, mirror, settings
|
from jira_hub.core import settings
|
||||||
|
from jira_hub.modules.jira import importer, mirror
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
23
tests/test_layout.py
Normal file
23
tests/test_layout.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""Раскладка пакетов: чистый импорт и пути, зависящие от места файла."""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import jira_hub
|
||||||
|
|
||||||
|
|
||||||
|
def test_fresh_imports():
|
||||||
|
r = subprocess.run(
|
||||||
|
[sys.executable, "-c", "import jira_hub.core.api, jira_hub.core.mcp_server, jira_hub.cli"],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_web_app_path():
|
||||||
|
from jira_hub.core import api
|
||||||
|
assert api.WEB_APP == Path(jira_hub.__file__).parent / "web" / "app"
|
||||||
|
|
||||||
|
|
||||||
|
def test_hub_root():
|
||||||
|
from jira_hub.core import mcp_server
|
||||||
|
assert (mcp_server.HUB_ROOT / "pyproject.toml").is_file()
|
||||||
@ -9,8 +9,9 @@ from types import SimpleNamespace
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import api, commands, files, mcp_server, settings
|
from jira_hub.core import api, files, mcp_server, settings
|
||||||
from jira_hub.client import HubClient
|
from jira_hub.modules.jira import commands
|
||||||
|
from jira_hub.core.client import HubClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -811,7 +812,7 @@ def test_attachment_failed(downloads, monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_attachment_413(downloads, monkeypatch):
|
def test_attachment_413(downloads, monkeypatch):
|
||||||
from jira_hub.client import HubError
|
from jira_hub.core.client import HubError
|
||||||
att = _att("5", "запись.mp4", 26214400)
|
att = _att("5", "запись.mp4", 26214400)
|
||||||
hub = FakeHub(_detail(key=ATT_KEY, attachments=[att]), fetch=HubError(413, "too_large", "большой"))
|
hub = FakeHub(_detail(key=ATT_KEY, attachments=[att]), fetch=HubError(413, "too_large", "большой"))
|
||||||
_use(monkeypatch, hub)
|
_use(monkeypatch, hub)
|
||||||
@ -820,7 +821,7 @@ def test_attachment_413(downloads, monkeypatch):
|
|||||||
|
|
||||||
|
|
||||||
def test_attachment_409_passes_hub_text(downloads, monkeypatch):
|
def test_attachment_409_passes_hub_text(downloads, monkeypatch):
|
||||||
from jira_hub.client import HubError
|
from jira_hub.core.client import HubError
|
||||||
hub = FakeHub(_detail(key=ATT_KEY, attachments=[_att("5", "a.png", 1)]),
|
hub = FakeHub(_detail(key=ATT_KEY, attachments=[_att("5", "a.png", 1)]),
|
||||||
fetch=HubError(409, "agent_too_old", "агент v9 не умеет скачивать вложения — нужен v14"))
|
fetch=HubError(409, "agent_too_old", "агент v9 не умеет скачивать вложения — нужен v14"))
|
||||||
_use(monkeypatch, hub)
|
_use(monkeypatch, hub)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import db, mirror
|
from jira_hub.core import db
|
||||||
|
from jira_hub.modules.jira import mirror
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import mirror, projects, settings
|
from jira_hub.core import projects, settings
|
||||||
|
from jira_hub.modules.jira import mirror
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import hashlib
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import attachments, db, files, mirror, projects, queue, settings
|
from jira_hub.core import db, files, projects, queue, settings
|
||||||
from jira_hub.commands import CommandError
|
from jira_hub.modules.jira import attachments, mirror
|
||||||
from jira_hub.queue import ClaimedError
|
from jira_hub.modules.jira.commands import CommandError
|
||||||
|
from jira_hub.core.queue import ClaimedError
|
||||||
|
|
||||||
LT = {"связанные": "ASUPR_Связанные"}
|
LT = {"связанные": "ASUPR_Связанные"}
|
||||||
|
|
||||||
@ -207,7 +208,7 @@ def test_no_backup_for_refresh_batch(conn):
|
|||||||
|
|
||||||
|
|
||||||
def test_make_backup_rotates(tmp_path, open_db):
|
def test_make_backup_rotates(tmp_path, open_db):
|
||||||
from jira_hub import backup
|
from jira_hub.core import backup
|
||||||
conn = open_db(tmp_path / "hub.sqlite")
|
conn = open_db(tmp_path / "hub.sqlite")
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
backup.make_backup(conn, tmp_path / "backups", f"b{i}", keep=2)
|
backup.make_backup(conn, tmp_path / "backups", f"b{i}", keep=2)
|
||||||
@ -218,7 +219,7 @@ def test_make_backup_rotates(tmp_path, open_db):
|
|||||||
|
|
||||||
def test_rotation_keeps_schema_migration_copy(tmp_path, open_db):
|
def test_rotation_keeps_schema_migration_copy(tmp_path, open_db):
|
||||||
"""Копия базы перед миграцией схемы не участвует в ротации копий пакетов (M-1 финального ревью)."""
|
"""Копия базы перед миграцией схемы не участвует в ротации копий пакетов (M-1 финального ревью)."""
|
||||||
from jira_hub import backup
|
from jira_hub.core import backup
|
||||||
conn = open_db(tmp_path / "hub.sqlite")
|
conn = open_db(tmp_path / "hub.sqlite")
|
||||||
backup.make_backup(conn, tmp_path / "backups", "schema-v1", keep=0)
|
backup.make_backup(conn, tmp_path / "backups", "schema-v1", keep=0)
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
|
|||||||
@ -8,8 +8,8 @@ from datetime import datetime, timedelta, timezone
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import api, relay, settings
|
from jira_hub.core import api, relay, settings
|
||||||
from jira_hub.client import HubClient
|
from jira_hub.core.client import HubClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import release
|
from jira_hub.core import release
|
||||||
|
|
||||||
|
|
||||||
def agent_file(tmp_path, version, body="print('x')\n", name="agent.py"):
|
def agent_file(tmp_path, version, body="print('x')\n", name="agent.py"):
|
||||||
|
|||||||
@ -3,8 +3,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import queue, settings
|
from jira_hub.core import queue, settings
|
||||||
from jira_hub.commands import CommandError
|
from jira_hub.modules.jira.commands import CommandError
|
||||||
|
|
||||||
FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json"
|
FIXTURE = Path(__file__).parent / "fixtures" / "config.example.json"
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from jira_hub import api
|
from jira_hub.core import api
|
||||||
|
|
||||||
|
|
||||||
def _get(base, path):
|
def _get(base, path):
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from jira_hub import db, mirror, paths, settings
|
from jira_hub.core import db, paths, settings
|
||||||
|
from jira_hub.modules.jira import mirror
|
||||||
|
|
||||||
snap = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
snap = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||||
extras_path = Path(__file__).with_name("seed-extras.json")
|
extras_path = Path(__file__).with_name("seed-extras.json")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user