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 +22 -0
- annotide/cli.py +214 -0
- annotide/client.py +660 -0
- annotide/errors.py +58 -0
- annotide/mcp_server.py +213 -0
- annotide/models.py +2683 -0
- annotide/py.typed +0 -0
- annotide-0.1.0.dist-info/METADATA +18 -0
- annotide-0.1.0.dist-info/RECORD +14 -0
- annotide-0.1.0.dist-info/WHEEL +5 -0
- annotide-0.1.0.dist-info/entry_points.txt +2 -0
- annotide-0.1.0.dist-info/licenses/LICENSE +201 -0
- annotide-0.1.0.dist-info/licenses/NOTICE +7 -0
- annotide-0.1.0.dist-info/top_level.txt +1 -0
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())
|