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.
- yandex_cli-0.1.0.dist-info/METADATA +240 -0
- yandex_cli-0.1.0.dist-info/RECORD +115 -0
- yandex_cli-0.1.0.dist-info/WHEEL +4 -0
- yandex_cli-0.1.0.dist-info/entry_points.txt +3 -0
- yandex_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- ycli/__init__.py +13 -0
- ycli/cli.py +58 -0
- ycli/log.py +27 -0
- ycli/mcp.py +31 -0
- ycli/yandex/__init__.py +0 -0
- ycli/yandex/base.py +67 -0
- ycli/yandex/forms/__init__.py +1 -0
- ycli/yandex/forms/_base.py +17 -0
- ycli/yandex/forms/_clideps.py +20 -0
- ycli/yandex/forms/_deps.py +10 -0
- ycli/yandex/forms/_models.py +18 -0
- ycli/yandex/forms/answers/__init__.py +1 -0
- ycli/yandex/forms/answers/cli.py +25 -0
- ycli/yandex/forms/answers/client.py +57 -0
- ycli/yandex/forms/answers/mcp.py +15 -0
- ycli/yandex/forms/answers/models.py +54 -0
- ycli/yandex/forms/cli.py +16 -0
- ycli/yandex/forms/client.py +29 -0
- ycli/yandex/forms/mcp.py +13 -0
- ycli/yandex/forms/me/__init__.py +1 -0
- ycli/yandex/forms/me/cli.py +19 -0
- ycli/yandex/forms/me/client.py +25 -0
- ycli/yandex/forms/me/mcp.py +20 -0
- ycli/yandex/forms/me/models.py +18 -0
- ycli/yandex/forms/questions/__init__.py +1 -0
- ycli/yandex/forms/questions/cli.py +25 -0
- ycli/yandex/forms/questions/client.py +24 -0
- ycli/yandex/forms/questions/mcp.py +15 -0
- ycli/yandex/forms/questions/models.py +46 -0
- ycli/yandex/forms/surveys/__init__.py +1 -0
- ycli/yandex/forms/surveys/cli.py +26 -0
- ycli/yandex/forms/surveys/client.py +36 -0
- ycli/yandex/forms/surveys/mcp.py +26 -0
- ycli/yandex/forms/surveys/models.py +44 -0
- ycli/yandex/tracker/__init__.py +1 -0
- ycli/yandex/tracker/_base.py +14 -0
- ycli/yandex/tracker/_clideps.py +46 -0
- ycli/yandex/tracker/_deps.py +10 -0
- ycli/yandex/tracker/_models.py +48 -0
- ycli/yandex/tracker/changelog/__init__.py +1 -0
- ycli/yandex/tracker/changelog/cli.py +25 -0
- ycli/yandex/tracker/changelog/client.py +28 -0
- ycli/yandex/tracker/changelog/mcp.py +15 -0
- ycli/yandex/tracker/changelog/models.py +58 -0
- ycli/yandex/tracker/cli.py +26 -0
- ycli/yandex/tracker/client.py +39 -0
- ycli/yandex/tracker/comments/__init__.py +1 -0
- ycli/yandex/tracker/comments/cli.py +28 -0
- ycli/yandex/tracker/comments/client.py +37 -0
- ycli/yandex/tracker/comments/mcp.py +15 -0
- ycli/yandex/tracker/comments/models.py +34 -0
- ycli/yandex/tracker/issues/__init__.py +1 -0
- ycli/yandex/tracker/issues/cli.py +132 -0
- ycli/yandex/tracker/issues/client.py +92 -0
- ycli/yandex/tracker/issues/mcp.py +53 -0
- ycli/yandex/tracker/issues/models.py +73 -0
- ycli/yandex/tracker/issuetypes/__init__.py +1 -0
- ycli/yandex/tracker/issuetypes/cli.py +19 -0
- ycli/yandex/tracker/issuetypes/client.py +24 -0
- ycli/yandex/tracker/issuetypes/mcp.py +15 -0
- ycli/yandex/tracker/issuetypes/models.py +27 -0
- ycli/yandex/tracker/links/__init__.py +1 -0
- ycli/yandex/tracker/links/cli.py +43 -0
- ycli/yandex/tracker/links/client.py +37 -0
- ycli/yandex/tracker/links/mcp.py +15 -0
- ycli/yandex/tracker/links/models.py +56 -0
- ycli/yandex/tracker/linktypes/__init__.py +1 -0
- ycli/yandex/tracker/linktypes/cli.py +19 -0
- ycli/yandex/tracker/linktypes/client.py +24 -0
- ycli/yandex/tracker/linktypes/mcp.py +15 -0
- ycli/yandex/tracker/linktypes/models.py +28 -0
- ycli/yandex/tracker/mcp.py +23 -0
- ycli/yandex/tracker/priorities/__init__.py +1 -0
- ycli/yandex/tracker/priorities/cli.py +19 -0
- ycli/yandex/tracker/priorities/client.py +24 -0
- ycli/yandex/tracker/priorities/mcp.py +15 -0
- ycli/yandex/tracker/priorities/models.py +27 -0
- ycli/yandex/tracker/transitions/__init__.py +1 -0
- ycli/yandex/tracker/transitions/cli.py +34 -0
- ycli/yandex/tracker/transitions/client.py +52 -0
- ycli/yandex/tracker/transitions/mcp.py +15 -0
- ycli/yandex/tracker/transitions/models.py +27 -0
- ycli/yandex/tracker/worklog/__init__.py +1 -0
- ycli/yandex/tracker/worklog/cli.py +21 -0
- ycli/yandex/tracker/worklog/client.py +24 -0
- ycli/yandex/tracker/worklog/mcp.py +15 -0
- ycli/yandex/tracker/worklog/models.py +36 -0
- ycli/yandex/transport.py +116 -0
- ycli/yandex/wiki/__init__.py +1 -0
- ycli/yandex/wiki/_base.py +8 -0
- ycli/yandex/wiki/_clideps.py +20 -0
- ycli/yandex/wiki/_deps.py +10 -0
- ycli/yandex/wiki/attachments/__init__.py +0 -0
- ycli/yandex/wiki/attachments/cli.py +20 -0
- ycli/yandex/wiki/attachments/client.py +29 -0
- ycli/yandex/wiki/attachments/mcp.py +15 -0
- ycli/yandex/wiki/attachments/models.py +32 -0
- ycli/yandex/wiki/cli.py +14 -0
- ycli/yandex/wiki/client.py +27 -0
- ycli/yandex/wiki/comments/__init__.py +0 -0
- ycli/yandex/wiki/comments/cli.py +20 -0
- ycli/yandex/wiki/comments/client.py +29 -0
- ycli/yandex/wiki/comments/mcp.py +15 -0
- ycli/yandex/wiki/comments/models.py +40 -0
- ycli/yandex/wiki/mcp.py +11 -0
- ycli/yandex/wiki/pages/__init__.py +0 -0
- ycli/yandex/wiki/pages/cli.py +64 -0
- ycli/yandex/wiki/pages/client.py +74 -0
- ycli/yandex/wiki/pages/mcp.py +32 -0
- 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
|
ycli/yandex/forms/cli.py
ADDED
|
@@ -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())
|
ycli/yandex/forms/mcp.py
ADDED
|
@@ -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()
|