annotide 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.
annotide/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """Python SDK for the Annotide API (API-3).
2
+
3
+ from annotide import Client
4
+
5
+ with Client("https://annotate.example.com", api_key="...") as client:
6
+ snapshot = client.take_snapshot(project_id, "2026-q3")
7
+ client.export(project_id, "coco", "dataset.zip", snapshot_id=snapshot["id"])
8
+ """
9
+
10
+ from annotide.client import Client
11
+ from annotide.errors import AnnotationError, ApiError, JobFailedError, JobTimeoutError
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ __all__ = [
16
+ "AnnotationError",
17
+ "ApiError",
18
+ "Client",
19
+ "JobFailedError",
20
+ "JobTimeoutError",
21
+ "__version__",
22
+ ]
annotide/cli.py ADDED
@@ -0,0 +1,214 @@
1
+ """`annotation` — command line over the SDK (API-3).
2
+
3
+ Reads the key from `ANNOTIDE_API_KEY` only (a flag would end up in shell
4
+ history and `ps`); the site from `--url` or `ANNOTIDE_URL`. Single objects
5
+ print as indented JSON, lists as JSON Lines, so output pipes into `jq`.
6
+
7
+ annotide projects list
8
+ annotide snapshots create <project> "2026-q3" --split 0.8,0.1,0.1
9
+ annotide export <project> --format coco --snapshot <id> -o dataset.zip
10
+ annotide import <project> labels.json --format coco --dry-run
11
+ annotide mcp # MCP server for AI agents over stdio (API-8)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import sys
19
+ from collections.abc import Callable, Iterable
20
+ from typing import Any
21
+
22
+ from annotide import __version__
23
+ from annotide.client import URL_ENV, Client
24
+ from annotide.errors import AnnotationError
25
+
26
+ ClientFactory = Callable[[str | None], Client]
27
+
28
+
29
+ def _print_one(value: Any) -> None:
30
+ print(json.dumps(value, indent=2, ensure_ascii=False))
31
+
32
+
33
+ def _print_many(values: Iterable[Any]) -> None:
34
+ for value in values:
35
+ print(json.dumps(value, ensure_ascii=False))
36
+
37
+
38
+ def _split(text: str) -> dict[str, float]:
39
+ try:
40
+ train, val, test = (float(part) for part in text.split(","))
41
+ except ValueError as exc:
42
+ raise argparse.ArgumentTypeError("expected three ratios, e.g. 0.8,0.1,0.1") from exc
43
+ return {"train": train, "val": val, "test": test}
44
+
45
+
46
+ def _class_map(text: str) -> dict[str, str | None]:
47
+ try:
48
+ value = json.loads(text)
49
+ except ValueError as exc:
50
+ raise argparse.ArgumentTypeError(f"not JSON: {exc}") from exc
51
+ if not isinstance(value, dict):
52
+ raise argparse.ArgumentTypeError('expected a JSON object, e.g. {"car": "vehicle"}')
53
+ return value
54
+
55
+
56
+ def build_parser() -> argparse.ArgumentParser:
57
+ parser = argparse.ArgumentParser(
58
+ prog="annotide",
59
+ description="Annotide command line. The API key is read from ANNOTIDE_API_KEY.",
60
+ )
61
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
62
+ parser.add_argument("--url", help=f"site root, e.g. https://annotate.example.com (${URL_ENV})")
63
+ commands = parser.add_subparsers(dest="command", required=True, metavar="command")
64
+
65
+ commands.add_parser("whoami", help="the user or service account the key acts as")
66
+ commands.add_parser(
67
+ "mcp",
68
+ help="serve the Model Context Protocol over stdio for AI agents (needs the mcp extra)",
69
+ )
70
+
71
+ projects = commands.add_parser("projects", help="projects").add_subparsers(
72
+ dest="action", required=True
73
+ )
74
+ projects.add_parser("list", help="projects you are a member of")
75
+ stats = projects.add_parser("stats", help="dashboard numbers of one project")
76
+ stats.add_argument("project")
77
+
78
+ items = commands.add_parser("items", help="items").add_subparsers(dest="action", required=True)
79
+ items_list = items.add_parser("list", help="a project's items")
80
+ items_list.add_argument("project")
81
+ items_list.add_argument("--status")
82
+ items_list.add_argument("--media-type")
83
+ items_list.add_argument("--q", help="path substring")
84
+
85
+ snapshots = commands.add_parser("snapshots", help="dataset snapshots").add_subparsers(
86
+ dest="action", required=True
87
+ )
88
+ snapshots_list = snapshots.add_parser("list", help="a project's snapshots")
89
+ snapshots_list.add_argument("project")
90
+ create = snapshots.add_parser("create", help="freeze a snapshot and wait for it")
91
+ create.add_argument("project")
92
+ create.add_argument("name")
93
+ create.add_argument("--split", type=_split, help="train,val,test ratios, e.g. 0.8,0.1,0.1")
94
+ create.add_argument("--seed", type=int, default=0, help="split seed (default 0)")
95
+ create.add_argument("--group-by", help='keep groups together: "folder" or "meta.<key>"')
96
+ create.add_argument("--timeout", type=float, default=600.0)
97
+
98
+ export = commands.add_parser("export", help="export and download an archive")
99
+ export.add_argument("project")
100
+ export.add_argument("--format", required=True, help="coco, yolo or native")
101
+ export.add_argument("-o", "--output", required=True, help="file or directory to write")
102
+ export.add_argument("--snapshot", help="snapshot id (recommended: reproducible)")
103
+ export.add_argument("--split", choices=["train", "val", "test"])
104
+ export.add_argument("--timeout", type=float, default=1800.0)
105
+
106
+ imp = commands.add_parser("import", help="upload an annotation file and import it")
107
+ imp.add_argument("project")
108
+ imp.add_argument("file")
109
+ imp.add_argument("--format", required=True, help="coco, yolo, voc, cvat or label_studio")
110
+ imp.add_argument("--status", choices=["submitted", "draft"])
111
+ imp.add_argument("--class-map", type=_class_map, help='JSON, e.g. {"car": "vehicle"}')
112
+ imp.add_argument("--dry-run", action="store_true", help="report only, write nothing")
113
+ imp.add_argument("--timeout", type=float, default=1800.0)
114
+
115
+ jobs = commands.add_parser("jobs", help="background jobs").add_subparsers(
116
+ dest="action", required=True
117
+ )
118
+ jobs_list = jobs.add_parser("list", help="a project's jobs")
119
+ jobs_list.add_argument("project")
120
+ jobs_list.add_argument("--status")
121
+ jobs_list.add_argument("--type")
122
+ for name, text in (("get", "one job"), ("wait", "wait until a job finishes")):
123
+ sub = jobs.add_parser(name, help=text)
124
+ sub.add_argument("job")
125
+ if name == "wait":
126
+ sub.add_argument("--timeout", type=float, default=600.0)
127
+ return parser
128
+
129
+
130
+ def run(args: argparse.Namespace, client: Client) -> None:
131
+ command, action = args.command, getattr(args, "action", None)
132
+ if command == "whoami":
133
+ _print_one(client.me())
134
+ elif command == "projects" and action == "list":
135
+ _print_many(client.list_projects())
136
+ elif command == "projects" and action == "stats":
137
+ _print_one(client.get_stats(args.project))
138
+ elif command == "items":
139
+ _print_many(
140
+ client.list_items(
141
+ args.project, status=args.status, media_type=args.media_type, q=args.q
142
+ )
143
+ )
144
+ elif command == "snapshots" and action == "list":
145
+ _print_many(client.list_snapshots(args.project))
146
+ elif command == "snapshots" and action == "create":
147
+ split = None
148
+ if args.split is not None:
149
+ split = {**args.split, "seed": args.seed, "group_by": args.group_by}
150
+ _print_one(client.take_snapshot(args.project, args.name, split=split, timeout=args.timeout))
151
+ elif command == "export":
152
+ path = client.export(
153
+ args.project,
154
+ args.format,
155
+ args.output,
156
+ snapshot_id=args.snapshot,
157
+ split=args.split,
158
+ timeout=args.timeout,
159
+ )
160
+ print(path)
161
+ elif command == "import":
162
+ _print_one(
163
+ client.import_file(
164
+ args.project,
165
+ args.file,
166
+ args.format,
167
+ class_mapping=args.class_map,
168
+ status=args.status,
169
+ dry_run=args.dry_run,
170
+ timeout=args.timeout,
171
+ )
172
+ )
173
+ elif command == "jobs" and action == "list":
174
+ _print_many(client.list_jobs(args.project, status=args.status, type=args.type))
175
+ elif command == "jobs" and action == "get":
176
+ _print_one(client.get_job(args.job))
177
+ elif command == "jobs" and action == "wait":
178
+ _print_one(client.wait_for_job(args.job, timeout=args.timeout))
179
+ else: # pragma: no cover - argparse rejects anything else
180
+ raise AnnotationError(f"unknown command {command} {action or ''}".rstrip())
181
+
182
+
183
+ def _serve_mcp(url: str | None) -> int:
184
+ """`annotide mcp`: the MCP server needs the `mcp` extra and owns stdout."""
185
+ try:
186
+ from annotide import mcp_server
187
+ except ImportError:
188
+ print(
189
+ 'annotation: the MCP server needs the extra: pip install "annotide[mcp]"',
190
+ file=sys.stderr,
191
+ )
192
+ return 2
193
+ mcp_server.build_server(lambda: Client(url)).run("stdio")
194
+ return 0
195
+
196
+
197
+ def main(argv: list[str] | None = None, client_factory: ClientFactory | None = None) -> int:
198
+ args = build_parser().parse_args(argv)
199
+ if args.command == "mcp":
200
+ return _serve_mcp(args.url)
201
+ factory: ClientFactory = client_factory or (lambda url: Client(url))
202
+ try:
203
+ with factory(args.url) as client:
204
+ run(args, client)
205
+ except AnnotationError as exc:
206
+ print(f"annotation: {exc}", file=sys.stderr)
207
+ return 1
208
+ except KeyboardInterrupt:
209
+ return 130
210
+ return 0
211
+
212
+
213
+ if __name__ == "__main__":
214
+ raise SystemExit(main())