yandex-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. yandex_cli-0.1.0.dist-info/METADATA +240 -0
  2. yandex_cli-0.1.0.dist-info/RECORD +115 -0
  3. yandex_cli-0.1.0.dist-info/WHEEL +4 -0
  4. yandex_cli-0.1.0.dist-info/entry_points.txt +3 -0
  5. yandex_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. ycli/__init__.py +13 -0
  7. ycli/cli.py +58 -0
  8. ycli/log.py +27 -0
  9. ycli/mcp.py +31 -0
  10. ycli/yandex/__init__.py +0 -0
  11. ycli/yandex/base.py +67 -0
  12. ycli/yandex/forms/__init__.py +1 -0
  13. ycli/yandex/forms/_base.py +17 -0
  14. ycli/yandex/forms/_clideps.py +20 -0
  15. ycli/yandex/forms/_deps.py +10 -0
  16. ycli/yandex/forms/_models.py +18 -0
  17. ycli/yandex/forms/answers/__init__.py +1 -0
  18. ycli/yandex/forms/answers/cli.py +25 -0
  19. ycli/yandex/forms/answers/client.py +57 -0
  20. ycli/yandex/forms/answers/mcp.py +15 -0
  21. ycli/yandex/forms/answers/models.py +54 -0
  22. ycli/yandex/forms/cli.py +16 -0
  23. ycli/yandex/forms/client.py +29 -0
  24. ycli/yandex/forms/mcp.py +13 -0
  25. ycli/yandex/forms/me/__init__.py +1 -0
  26. ycli/yandex/forms/me/cli.py +19 -0
  27. ycli/yandex/forms/me/client.py +25 -0
  28. ycli/yandex/forms/me/mcp.py +20 -0
  29. ycli/yandex/forms/me/models.py +18 -0
  30. ycli/yandex/forms/questions/__init__.py +1 -0
  31. ycli/yandex/forms/questions/cli.py +25 -0
  32. ycli/yandex/forms/questions/client.py +24 -0
  33. ycli/yandex/forms/questions/mcp.py +15 -0
  34. ycli/yandex/forms/questions/models.py +46 -0
  35. ycli/yandex/forms/surveys/__init__.py +1 -0
  36. ycli/yandex/forms/surveys/cli.py +26 -0
  37. ycli/yandex/forms/surveys/client.py +36 -0
  38. ycli/yandex/forms/surveys/mcp.py +26 -0
  39. ycli/yandex/forms/surveys/models.py +44 -0
  40. ycli/yandex/tracker/__init__.py +1 -0
  41. ycli/yandex/tracker/_base.py +14 -0
  42. ycli/yandex/tracker/_clideps.py +46 -0
  43. ycli/yandex/tracker/_deps.py +10 -0
  44. ycli/yandex/tracker/_models.py +48 -0
  45. ycli/yandex/tracker/changelog/__init__.py +1 -0
  46. ycli/yandex/tracker/changelog/cli.py +25 -0
  47. ycli/yandex/tracker/changelog/client.py +28 -0
  48. ycli/yandex/tracker/changelog/mcp.py +15 -0
  49. ycli/yandex/tracker/changelog/models.py +58 -0
  50. ycli/yandex/tracker/cli.py +26 -0
  51. ycli/yandex/tracker/client.py +39 -0
  52. ycli/yandex/tracker/comments/__init__.py +1 -0
  53. ycli/yandex/tracker/comments/cli.py +28 -0
  54. ycli/yandex/tracker/comments/client.py +37 -0
  55. ycli/yandex/tracker/comments/mcp.py +15 -0
  56. ycli/yandex/tracker/comments/models.py +34 -0
  57. ycli/yandex/tracker/issues/__init__.py +1 -0
  58. ycli/yandex/tracker/issues/cli.py +132 -0
  59. ycli/yandex/tracker/issues/client.py +92 -0
  60. ycli/yandex/tracker/issues/mcp.py +53 -0
  61. ycli/yandex/tracker/issues/models.py +73 -0
  62. ycli/yandex/tracker/issuetypes/__init__.py +1 -0
  63. ycli/yandex/tracker/issuetypes/cli.py +19 -0
  64. ycli/yandex/tracker/issuetypes/client.py +24 -0
  65. ycli/yandex/tracker/issuetypes/mcp.py +15 -0
  66. ycli/yandex/tracker/issuetypes/models.py +27 -0
  67. ycli/yandex/tracker/links/__init__.py +1 -0
  68. ycli/yandex/tracker/links/cli.py +43 -0
  69. ycli/yandex/tracker/links/client.py +37 -0
  70. ycli/yandex/tracker/links/mcp.py +15 -0
  71. ycli/yandex/tracker/links/models.py +56 -0
  72. ycli/yandex/tracker/linktypes/__init__.py +1 -0
  73. ycli/yandex/tracker/linktypes/cli.py +19 -0
  74. ycli/yandex/tracker/linktypes/client.py +24 -0
  75. ycli/yandex/tracker/linktypes/mcp.py +15 -0
  76. ycli/yandex/tracker/linktypes/models.py +28 -0
  77. ycli/yandex/tracker/mcp.py +23 -0
  78. ycli/yandex/tracker/priorities/__init__.py +1 -0
  79. ycli/yandex/tracker/priorities/cli.py +19 -0
  80. ycli/yandex/tracker/priorities/client.py +24 -0
  81. ycli/yandex/tracker/priorities/mcp.py +15 -0
  82. ycli/yandex/tracker/priorities/models.py +27 -0
  83. ycli/yandex/tracker/transitions/__init__.py +1 -0
  84. ycli/yandex/tracker/transitions/cli.py +34 -0
  85. ycli/yandex/tracker/transitions/client.py +52 -0
  86. ycli/yandex/tracker/transitions/mcp.py +15 -0
  87. ycli/yandex/tracker/transitions/models.py +27 -0
  88. ycli/yandex/tracker/worklog/__init__.py +1 -0
  89. ycli/yandex/tracker/worklog/cli.py +21 -0
  90. ycli/yandex/tracker/worklog/client.py +24 -0
  91. ycli/yandex/tracker/worklog/mcp.py +15 -0
  92. ycli/yandex/tracker/worklog/models.py +36 -0
  93. ycli/yandex/transport.py +116 -0
  94. ycli/yandex/wiki/__init__.py +1 -0
  95. ycli/yandex/wiki/_base.py +8 -0
  96. ycli/yandex/wiki/_clideps.py +20 -0
  97. ycli/yandex/wiki/_deps.py +10 -0
  98. ycli/yandex/wiki/attachments/__init__.py +0 -0
  99. ycli/yandex/wiki/attachments/cli.py +20 -0
  100. ycli/yandex/wiki/attachments/client.py +29 -0
  101. ycli/yandex/wiki/attachments/mcp.py +15 -0
  102. ycli/yandex/wiki/attachments/models.py +32 -0
  103. ycli/yandex/wiki/cli.py +14 -0
  104. ycli/yandex/wiki/client.py +27 -0
  105. ycli/yandex/wiki/comments/__init__.py +0 -0
  106. ycli/yandex/wiki/comments/cli.py +20 -0
  107. ycli/yandex/wiki/comments/client.py +29 -0
  108. ycli/yandex/wiki/comments/mcp.py +15 -0
  109. ycli/yandex/wiki/comments/models.py +40 -0
  110. ycli/yandex/wiki/mcp.py +11 -0
  111. ycli/yandex/wiki/pages/__init__.py +0 -0
  112. ycli/yandex/wiki/pages/cli.py +64 -0
  113. ycli/yandex/wiki/pages/client.py +74 -0
  114. ycli/yandex/wiki/pages/mcp.py +32 -0
  115. ycli/yandex/wiki/pages/models.py +82 -0
@@ -0,0 +1 @@
1
+ """Forms /surveys/{id}/answers resource package."""
@@ -0,0 +1,25 @@
1
+ """`forms answers` commands."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from ycli.yandex.forms._clideps import forms_client
9
+
10
+ app = typer.Typer(name="answers", help="Forms answers.", no_args_is_help=True)
11
+
12
+ SurveyIdArg = Annotated[
13
+ str, typer.Argument(metavar="SURVEY_ID", help="Form id, e.g. 6818ceffe010db4f59d11329.")
14
+ ]
15
+
16
+
17
+ @app.callback()
18
+ def _group() -> None:
19
+ """Group anchor — forces subcommand dispatch (no eager DI, so --help stays cred-free)."""
20
+
21
+
22
+ @app.command("list")
23
+ def list_(ctx: typer.Context, survey_id: SurveyIdArg) -> None:
24
+ """List ALL of a form's responses (drains every page via the next cursor)."""
25
+ print(forms_client(ctx).answers.list_all(survey_id).model_dump_json(by_alias=True))
@@ -0,0 +1,57 @@
1
+ """Declarative Forms answers client (uplink) — transport ONLY.
2
+
3
+ NOTE: no ``from __future__ import annotations`` — uplink reads annotations eagerly.
4
+ """
5
+ from urllib.parse import urljoin
6
+
7
+ import uplink
8
+
9
+ from ycli.yandex.forms._base import FormsResource
10
+ from ycli.yandex.forms.answers.models import AnswersResponse
11
+
12
+
13
+ class AnswersClient(FormsResource):
14
+ """Declarative HTTP for ``/surveys/{id}/answers``."""
15
+
16
+ @uplink.timeout(30)
17
+ @uplink.returns.json()
18
+ @uplink.get("surveys/{survey_id}/answers")
19
+ def list(self, survey_id: uplink.Path) -> AnswersResponse: # ty: ignore[empty-body]
20
+ """``GET /surveys/{id}/answers`` → the ``{columns, answers, next}`` envelope (verbatim).
21
+
22
+ Example:
23
+ >>> client = FormsClient.from_env() # doctest: +SKIP
24
+ >>> client.answers.list(survey_id="686d0a1b2c3d4e5f").columns[0].slug # doctest: +SKIP
25
+ 'answer_short_text_1'
26
+ """
27
+
28
+ def list_all(self, survey_id: str) -> AnswersResponse:
29
+ """Drain *every* page of responses into one merged envelope.
30
+
31
+ The single-page :meth:`list` under-reports — the API paginates via the ``id``
32
+ cursor and hands the next page back as ``next.next_url`` (null when exhausted).
33
+ Follow that link verbatim (HATEOAS-style — no fragile cursor reconstruction)
34
+ until it is null, concatenating ``answers``. ``columns`` are taken from the
35
+ first page (identical across pages); the merged ``next`` is always ``None``.
36
+
37
+ Example:
38
+ >>> client = FormsClient.from_env() # doctest: +SKIP
39
+ >>> len(client.answers.list_all(survey_id="686d0a1b2c3d4e5f").answers) # doctest: +SKIP
40
+ 317
41
+ """
42
+ page = self.list(survey_id)
43
+ columns = page.columns
44
+ answers = list(page.answers)
45
+ seen: set[str] = set()
46
+ nxt = page.next
47
+ while isinstance(nxt, dict) and nxt.get("next_url"):
48
+ url = urljoin(self.base_url.rstrip("/") + "/", str(nxt["next_url"]))
49
+ if url in seen: # defensive: a server pointing at itself must not hang us
50
+ break
51
+ seen.add(url)
52
+ resp = self._session.get(url)
53
+ resp.raise_for_status()
54
+ page = AnswersResponse.model_validate(resp.json())
55
+ answers.extend(page.answers)
56
+ nxt = page.next
57
+ return AnswersResponse(columns=columns, answers=answers, next=None)
@@ -0,0 +1,15 @@
1
+ """Forms answers FastMCP tool (reads-only)."""
2
+ from fastmcp import FastMCP
3
+ from fastmcp.dependencies import Depends
4
+
5
+ from ycli.yandex.forms._deps import RO, TAGS, forms_client
6
+ from ycli.yandex.forms.answers.models import AnswersResponse
7
+ from ycli.yandex.forms.client import FormsClient
8
+
9
+ mcp = FastMCP("forms-answers")
10
+
11
+
12
+ @mcp.tool(name="answers_list", annotations=RO, tags=TAGS)
13
+ def list_(survey_id: str, client: FormsClient = Depends(forms_client)) -> AnswersResponse:
14
+ """ALL of a form's responses (drains every page via the next cursor)."""
15
+ return client.answers.list_all(survey_id)
@@ -0,0 +1,54 @@
1
+ """Pydantic models for Forms answers (Column + Answer + AnswersResponse envelope)."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from ycli.yandex.forms._models import _Lenient
7
+
8
+
9
+ class Column(_Lenient):
10
+ """An answers-table column descriptor (``…/answers`` → ``columns[]``).
11
+
12
+ Example:
13
+ >>> Column.model_validate({"id": 1, "slug": "s", "type": "string", "text": "T"}).text
14
+ 'T'
15
+ """
16
+
17
+ id: int | None = None
18
+ slug: str | None = None
19
+ type: str | None = None
20
+ text: str | None = None
21
+ has_scores: bool | None = None
22
+
23
+
24
+ class Answer(_Lenient):
25
+ """A single form response (``…/answers`` → ``answers[]``).
26
+
27
+ ``data`` is **positional**, aligned to ``columns``; each element is a
28
+ ``{"value": …}`` dict or ``null``. Passed through verbatim as ``Any``
29
+ (``value`` is a ``str`` or ``list[str]``).
30
+
31
+ Example:
32
+ >>> Answer.model_validate({"id": 9, "created": "2026-01-01", "data": [{"value": "x"}]}).data
33
+ [{'value': 'x'}]
34
+ """
35
+
36
+ id: int | None = None
37
+ created: str | None = None
38
+ data: list[Any] = []
39
+
40
+
41
+ class AnswersResponse(_Lenient):
42
+ """Envelope for ``GET …/answers`` — ``{columns, answers, next}``.
43
+
44
+ ``next`` is ``{"next_url": …}`` or ``null`` (a pagination cursor), passed
45
+ through as ``Any``.
46
+
47
+ Example:
48
+ >>> AnswersResponse.model_validate({"columns": [], "answers": [], "next": None}).answers
49
+ []
50
+ """
51
+
52
+ columns: list[Column] = []
53
+ answers: list[Answer] = []
54
+ next: Any = None
@@ -0,0 +1,16 @@
1
+ """Yandex Forms CLI — mounts the per-resource sub-apps (lazy client DI via _clideps)."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+
6
+ from ycli.yandex.forms.answers.cli import app as answers_app
7
+ from ycli.yandex.forms.me.cli import app as me_app
8
+ from ycli.yandex.forms.questions.cli import app as questions_app
9
+ from ycli.yandex.forms.surveys.cli import app as surveys_app
10
+
11
+ app = typer.Typer(name="forms", help="Yandex Forms read.", no_args_is_help=True)
12
+
13
+ app.add_typer(me_app)
14
+ app.add_typer(surveys_app)
15
+ app.add_typer(questions_app)
16
+ app.add_typer(answers_app)
@@ -0,0 +1,29 @@
1
+ """FormsClient — composition root over the forms resource clients (one shared session)."""
2
+ from __future__ import annotations
3
+
4
+ import requests
5
+
6
+ from ycli.yandex.base import session_from_env
7
+ from ycli.yandex.forms.answers.client import AnswersClient
8
+ from ycli.yandex.forms.me.client import MeClient
9
+ from ycli.yandex.forms.questions.client import QuestionsClient
10
+ from ycli.yandex.forms.surveys.client import SurveysClient
11
+
12
+
13
+ class FormsClient:
14
+ """Holds the per-resource forms clients, all sharing one ``requests.Session``.
15
+
16
+ Example:
17
+ >>> FormsClient.from_env().me.get() # doctest: +SKIP
18
+ """
19
+
20
+ def __init__(self, *, session: requests.Session) -> None:
21
+ self.me = MeClient(session=session)
22
+ self.surveys = SurveysClient(session=session)
23
+ self.questions = QuestionsClient(session=session)
24
+ self.answers = AnswersClient(session=session)
25
+
26
+ @classmethod
27
+ def from_env(cls) -> FormsClient:
28
+ """Build all sub-clients from one env-resolved session."""
29
+ return cls(session=session_from_env())
@@ -0,0 +1,13 @@
1
+ """Forms FastMCP subserver — mounts the per-resource tool servers (reads-only)."""
2
+ from fastmcp import FastMCP
3
+
4
+ from ycli.yandex.forms.answers.mcp import mcp as answers_mcp
5
+ from ycli.yandex.forms.me.mcp import mcp as me_mcp
6
+ from ycli.yandex.forms.questions.mcp import mcp as questions_mcp
7
+ from ycli.yandex.forms.surveys.mcp import mcp as surveys_mcp
8
+
9
+ mcp = FastMCP("forms")
10
+ mcp.mount(me_mcp)
11
+ mcp.mount(surveys_mcp)
12
+ mcp.mount(questions_mcp)
13
+ mcp.mount(answers_mcp)
@@ -0,0 +1 @@
1
+ """Forms /users/me resource package."""
@@ -0,0 +1,19 @@
1
+ """`forms me` commands."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+
6
+ from ycli.yandex.forms._clideps import forms_client
7
+
8
+ app = typer.Typer(name="me", help="Forms authenticated user.", no_args_is_help=True)
9
+
10
+
11
+ @app.callback()
12
+ def _group() -> None:
13
+ """Group anchor — forces subcommand dispatch (no eager DI, so --help stays cred-free)."""
14
+
15
+
16
+ @app.command()
17
+ def get(ctx: typer.Context) -> None:
18
+ """Print the authenticated user (a safe auth probe)."""
19
+ print(forms_client(ctx).me.get().model_dump_json(by_alias=True))
@@ -0,0 +1,25 @@
1
+ """Declarative Forms /users/me client (uplink) — transport ONLY.
2
+
3
+ NOTE: do NOT add ``from __future__ import annotations`` — uplink reads parameter
4
+ annotations eagerly.
5
+ """
6
+ import uplink
7
+
8
+ from ycli.yandex.forms._base import FormsResource
9
+ from ycli.yandex.forms.me.models import User
10
+
11
+
12
+ class MeClient(FormsResource):
13
+ """Declarative HTTP for ``/users/me``."""
14
+
15
+ @uplink.timeout(30)
16
+ @uplink.returns.json()
17
+ @uplink.get("users/me")
18
+ def get(self) -> User: # ty: ignore[empty-body]
19
+ """``GET /users/me`` → the authenticated ``User`` (a safe auth probe).
20
+
21
+ Example:
22
+ >>> client = FormsClient.from_env() # doctest: +SKIP
23
+ >>> client.me.get().email # doctest: +SKIP
24
+ 'znatnov.s@example.com'
25
+ """
@@ -0,0 +1,20 @@
1
+ """Forms /users/me FastMCP tool (reads-only) — Depends DI, native error handling."""
2
+ from fastmcp import FastMCP
3
+ from fastmcp.dependencies import Depends
4
+
5
+ from ycli.yandex.forms._deps import RO, TAGS, forms_client
6
+ from ycli.yandex.forms.client import FormsClient
7
+ from ycli.yandex.forms.me.models import User
8
+
9
+ mcp = FastMCP("forms-me")
10
+
11
+
12
+ @mcp.tool(name="me_get", annotations=RO, tags=TAGS)
13
+ def get(client: FormsClient = Depends(forms_client)) -> User:
14
+ """The authenticated Yandex Forms user (a safe auth probe)."""
15
+ result = client.me.get()
16
+ # Forms models are fully lenient, so a 401/4xx deserializes into an all-None User
17
+ # instead of raising. Guard so the auth probe actually fails on failure.
18
+ if result.id is None:
19
+ raise ValueError("auth probe failed — empty user (check YANDEX_ID_OAUTH_TOKEN)")
20
+ return result
@@ -0,0 +1,18 @@
1
+ """Pydantic model for Forms /users/me (User)."""
2
+ from __future__ import annotations
3
+
4
+ from ycli.yandex.forms._models import _Lenient
5
+
6
+
7
+ class User(_Lenient):
8
+ """The authenticated user (``GET /v1/users/me``) — a safe auth probe.
9
+
10
+ Example:
11
+ >>> User.model_validate({"id": 1, "uid": "u", "cloud_uid": "c", "email": "e@x"}).email
12
+ 'e@x'
13
+ """
14
+
15
+ id: int | None = None
16
+ uid: str | None = None
17
+ cloud_uid: str | None = None
18
+ email: str | None = None
@@ -0,0 +1 @@
1
+ """Forms /surveys/{id}/questions resource package."""
@@ -0,0 +1,25 @@
1
+ """`forms questions` commands."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from ycli.yandex.forms._clideps import forms_client
9
+
10
+ app = typer.Typer(name="questions", help="Forms questions.", no_args_is_help=True)
11
+
12
+ SurveyIdArg = Annotated[
13
+ str, typer.Argument(metavar="SURVEY_ID", help="Form id, e.g. 6818ceffe010db4f59d11329.")
14
+ ]
15
+
16
+
17
+ @app.callback()
18
+ def _group() -> None:
19
+ """Group anchor — forces subcommand dispatch (no eager DI, so --help stays cred-free)."""
20
+
21
+
22
+ @app.command("list")
23
+ def list_(ctx: typer.Context, survey_id: SurveyIdArg) -> None:
24
+ """List a form's questions (the {pages} envelope)."""
25
+ print(forms_client(ctx).questions.list(survey_id).model_dump_json(by_alias=True))
@@ -0,0 +1,24 @@
1
+ """Declarative Forms questions client (uplink) — transport ONLY.
2
+
3
+ NOTE: no ``from __future__ import annotations`` — uplink reads annotations eagerly.
4
+ """
5
+ import uplink
6
+
7
+ from ycli.yandex.forms._base import FormsResource
8
+ from ycli.yandex.forms.questions.models import QuestionsResponse
9
+
10
+
11
+ class QuestionsClient(FormsResource):
12
+ """Declarative HTTP for ``/surveys/{id}/questions``."""
13
+
14
+ @uplink.timeout(30)
15
+ @uplink.returns.json()
16
+ @uplink.get("surveys/{survey_id}/questions")
17
+ def list(self, survey_id: uplink.Path) -> QuestionsResponse: # ty: ignore[empty-body]
18
+ """``GET /surveys/{id}/questions`` → the ``{pages}`` envelope (verbatim).
19
+
20
+ Example:
21
+ >>> client = FormsClient.from_env() # doctest: +SKIP
22
+ >>> client.questions.list(survey_id="686d0a1b2c3d4e5f").pages[0].items[0].slug # doctest: +SKIP
23
+ 'answer_short_text_1'
24
+ """
@@ -0,0 +1,15 @@
1
+ """Forms questions FastMCP tool (reads-only)."""
2
+ from fastmcp import FastMCP
3
+ from fastmcp.dependencies import Depends
4
+
5
+ from ycli.yandex.forms._deps import RO, TAGS, forms_client
6
+ from ycli.yandex.forms.client import FormsClient
7
+ from ycli.yandex.forms.questions.models import QuestionsResponse
8
+
9
+ mcp = FastMCP("forms-questions")
10
+
11
+
12
+ @mcp.tool(name="questions_list", annotations=RO, tags=TAGS)
13
+ def list_(survey_id: str, client: FormsClient = Depends(forms_client)) -> QuestionsResponse:
14
+ """A form's questions, grouped into pages (the {pages} envelope)."""
15
+ return client.questions.list(survey_id)
@@ -0,0 +1,46 @@
1
+ """Pydantic models for Forms questions (Question + Page + QuestionsResponse envelope)."""
2
+ from __future__ import annotations
3
+
4
+ from ycli.yandex.forms._models import _Lenient
5
+
6
+
7
+ class Question(_Lenient):
8
+ """A single question item within a page (``…/questions`` → ``pages[].items[]``).
9
+
10
+ ``id`` is an **int**. Type-specific fields (``data_source``, ``items``,
11
+ ``validators``, ``conditions``, …) are lenient-ignored.
12
+
13
+ Example:
14
+ >>> Question.model_validate({"id": 1, "slug": "s", "type": "string", "label": "L"}).slug
15
+ 's'
16
+ """
17
+
18
+ id: int | None = None
19
+ label: str | None = None
20
+ slug: str | None = None
21
+ type: str | None = None
22
+ hidden: bool | None = None
23
+ comment: str | None = None
24
+
25
+
26
+ class Page(_Lenient):
27
+ """A page grouping questions (``…/questions`` → ``pages[]``).
28
+
29
+ Example:
30
+ >>> Page.model_validate({"id": 7, "items": [{"id": 1}]}).items[0].id
31
+ 1
32
+ """
33
+
34
+ id: int | None = None
35
+ items: list[Question] = []
36
+
37
+
38
+ class QuestionsResponse(_Lenient):
39
+ """Envelope for ``GET …/questions`` — ``{pages:[Page]}``.
40
+
41
+ Example:
42
+ >>> QuestionsResponse.model_validate({"pages": [{"items": [{"id": 1}]}]}).pages[0].items[0].id
43
+ 1
44
+ """
45
+
46
+ pages: list[Page] = []
@@ -0,0 +1 @@
1
+ """Forms /surveys resource package."""
@@ -0,0 +1,26 @@
1
+ """`forms surveys` commands."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from ycli.yandex.forms._clideps import forms_client
9
+
10
+ app = typer.Typer(name="surveys", help="Forms surveys.", no_args_is_help=True)
11
+
12
+ SurveyIdArg = Annotated[
13
+ str, typer.Argument(metavar="SURVEY_ID", help="Form id, e.g. 6818ceffe010db4f59d11329.")
14
+ ]
15
+
16
+
17
+ @app.command("list")
18
+ def list_(ctx: typer.Context) -> None:
19
+ """List all forms (the {links, result} envelope)."""
20
+ print(forms_client(ctx).surveys.list().model_dump_json(by_alias=True))
21
+
22
+
23
+ @app.command()
24
+ def get(ctx: typer.Context, survey_id: SurveyIdArg) -> None:
25
+ """Print one form's settings for SURVEY_ID."""
26
+ print(forms_client(ctx).surveys.get(survey_id).model_dump_json(by_alias=True))
@@ -0,0 +1,36 @@
1
+ """Declarative Forms /surveys client (uplink) — transport ONLY.
2
+
3
+ NOTE: no ``from __future__ import annotations`` — uplink reads annotations eagerly.
4
+ """
5
+ import uplink
6
+
7
+ from ycli.yandex.forms._base import FormsResource
8
+ from ycli.yandex.forms.surveys.models import Survey, SurveyList
9
+
10
+
11
+ class SurveysClient(FormsResource):
12
+ """Declarative HTTP for ``/surveys`` (list envelope + single get)."""
13
+
14
+ @uplink.timeout(30)
15
+ @uplink.returns.json()
16
+ @uplink.get("surveys")
17
+ def list(self) -> SurveyList: # ty: ignore[empty-body]
18
+ """``GET /surveys`` → the ``{links, result}`` envelope (verbatim).
19
+
20
+ Example:
21
+ >>> client = FormsClient.from_env() # doctest: +SKIP
22
+ >>> client.surveys.list().result[0].name # doctest: +SKIP
23
+ 'Новая задача'
24
+ """
25
+
26
+ @uplink.timeout(30)
27
+ @uplink.returns.json()
28
+ @uplink.get("surveys/{survey_id}")
29
+ def get(self, survey_id: uplink.Path) -> Survey: # ty: ignore[empty-body]
30
+ """``GET /surveys/{id}`` → a single ``Survey`` (settings).
31
+
32
+ Example:
33
+ >>> client = FormsClient.from_env() # doctest: +SKIP
34
+ >>> client.surveys.get(survey_id="686d0a1b2c3d4e5f").is_published # doctest: +SKIP
35
+ True
36
+ """
@@ -0,0 +1,26 @@
1
+ """Forms /surveys FastMCP tools (reads-only)."""
2
+ from fastmcp import FastMCP
3
+ from fastmcp.dependencies import Depends
4
+
5
+ from ycli.yandex.forms._deps import RO, TAGS, forms_client
6
+ from ycli.yandex.forms.client import FormsClient
7
+ from ycli.yandex.forms.surveys.models import Survey, SurveyList
8
+
9
+ mcp = FastMCP("forms-surveys")
10
+
11
+
12
+ @mcp.tool(name="surveys_list", annotations=RO, tags=TAGS)
13
+ def list_(client: FormsClient = Depends(forms_client)) -> SurveyList:
14
+ """Every form (survey) the caller can see (the {links, result} envelope)."""
15
+ return client.surveys.list()
16
+
17
+
18
+ @mcp.tool(name="surveys_get", annotations=RO, tags=TAGS)
19
+ def get(survey_id: str, client: FormsClient = Depends(forms_client)) -> Survey:
20
+ """One form's settings by id."""
21
+ result = client.surveys.get(survey_id)
22
+ # A 404 deserializes into an all-None Survey (lenient model) rather than raising;
23
+ # turn that into a clean not-found error instead of a phantom empty object.
24
+ if result.id is None:
25
+ raise ValueError(f"survey {survey_id!r} not found (got empty response — check id or permissions)")
26
+ return result
@@ -0,0 +1,44 @@
1
+ """Pydantic models for Forms /surveys (Survey + SurveyList envelope)."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from ycli.yandex.forms._models import _Lenient
7
+
8
+
9
+ class Survey(_Lenient):
10
+ """A form/survey (``GET /v1/surveys`` item and ``GET /v1/surveys/{id}``).
11
+
12
+ ``id`` is a hex ObjectId **string** (not numeric); ``answers`` is an int
13
+ response count. Settings-only fields (``texts``, ``followers``, …) are
14
+ lenient-ignored.
15
+
16
+ Example:
17
+ >>> Survey.model_validate({"id": "686d", "name": "F", "answers": 444}).answers
18
+ 444
19
+ """
20
+
21
+ id: str | None = None
22
+ name: str | None = None
23
+ dir_id: str | None = None
24
+ collab_id: str | None = None
25
+ created: str | None = None
26
+ modified: str | None = None
27
+ language: str | None = None
28
+ is_published: bool | None = None
29
+ is_public: bool | None = None
30
+ is_banned: bool | None = None
31
+ answers: int | None = None
32
+ is_favourite: bool | None = None
33
+
34
+
35
+ class SurveyList(_Lenient):
36
+ """Envelope for ``GET /v1/surveys`` — ``{links, result:[Survey]}``.
37
+
38
+ Example:
39
+ >>> SurveyList.model_validate({"result": [{"id": "a"}]}).result[0].id
40
+ 'a'
41
+ """
42
+
43
+ links: dict[str, Any] = {}
44
+ result: list[Survey] = []
@@ -0,0 +1 @@
1
+ """Yandex Tracker SDK — pure per-resource clients (issues, comments, links, …)."""
@@ -0,0 +1,14 @@
1
+ """Per-domain base — carries the Tracker API base_url; resource clients inherit it.
2
+
3
+ NOTE: no ``from __future__ import annotations`` — resource clients subclass this and
4
+ uplink reads their method annotations eagerly. Keep this module annotation-eager too.
5
+ """
6
+ from typing import ClassVar
7
+
8
+ from ycli.yandex.base import BaseYandex
9
+
10
+
11
+ class TrackerResource(BaseYandex):
12
+ """Base for every Tracker resource client (inherits session DI + from_env)."""
13
+
14
+ base_url: ClassVar[str] = "https://api.tracker.yandex.net/v3"
@@ -0,0 +1,46 @@
1
+ """Lazy Typer DI for the tracker CLI + the ``--field key=value`` JSON-coerce helper."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+ import typer
8
+
9
+ from ycli.yandex.tracker.client import TrackerClient
10
+
11
+
12
+ def tracker_client(ctx: typer.Context) -> TrackerClient:
13
+ """Return the request-scoped TrackerClient, building it from env on first access.
14
+
15
+ Lazy so ``--help`` (which never runs a command body) needs no creds; cached on
16
+ ``ctx.obj`` so multiple accesses within one invocation share the session.
17
+
18
+ Example:
19
+ >>> tracker_client(ctx) # doctest: +SKIP
20
+ """
21
+ if ctx.obj is None:
22
+ ctx.obj = TrackerClient.from_env()
23
+ return ctx.obj
24
+
25
+
26
+ def parse_fields(items: list[str] | None) -> dict[str, Any]:
27
+ """Parse repeated ``--field key=value`` strings into a dict (gh ``-F`` model).
28
+
29
+ Each value is JSON-coerced (``123`` → int, ``true`` → bool, ``{"id":5}`` → object),
30
+ falling back to the raw string when it is not valid JSON. Raises ``typer.BadParameter``
31
+ when an item has no ``=``.
32
+
33
+ Example:
34
+ >>> parse_fields(["sprint=123", "name=hi"])
35
+ {'sprint': 123, 'name': 'hi'}
36
+ """
37
+ out: dict[str, Any] = {}
38
+ for item in items or []:
39
+ key, sep, raw = item.partition("=")
40
+ if not sep:
41
+ raise typer.BadParameter(f"--field must be key=value, got {item!r}")
42
+ try:
43
+ out[key] = json.loads(raw)
44
+ except json.JSONDecodeError:
45
+ out[key] = raw
46
+ return out
@@ -0,0 +1,10 @@
1
+ """FastMCP dependency provider for the tracker subserver — builds a TrackerClient per call."""
2
+ from ycli.yandex.tracker.client import TrackerClient
3
+
4
+ RO: dict[str, bool] = {"readOnlyHint": True}
5
+ TAGS: set[str] = {"tracker"}
6
+
7
+
8
+ def tracker_client() -> TrackerClient:
9
+ """Provide an env-built TrackerClient to tracker MCP tools (FastMCP caches within a call)."""
10
+ return TrackerClient.from_env()