tracelabel 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 (58) hide show
  1. tracelabel/__init__.py +1 -0
  2. tracelabel/__main__.py +4 -0
  3. tracelabel/api/__init__.py +0 -0
  4. tracelabel/api/app.py +40 -0
  5. tracelabel/api/labeling.py +202 -0
  6. tracelabel/api/models.py +92 -0
  7. tracelabel/api/routes.py +37 -0
  8. tracelabel/api/static.py +37 -0
  9. tracelabel/cli/__init__.py +0 -0
  10. tracelabel/cli/app.py +163 -0
  11. tracelabel/cli/commands.py +228 -0
  12. tracelabel/cli/options.py +30 -0
  13. tracelabel/cli/output.py +44 -0
  14. tracelabel/config/__init__.py +0 -0
  15. tracelabel/config/loader.py +51 -0
  16. tracelabel/config/models.py +96 -0
  17. tracelabel/config/presets.py +29 -0
  18. tracelabel/config/resolver.py +72 -0
  19. tracelabel/config/validation.py +144 -0
  20. tracelabel/ctf/__init__.py +0 -0
  21. tracelabel/ctf/content.py +35 -0
  22. tracelabel/ctf/hashing.py +31 -0
  23. tracelabel/ctf/models.py +78 -0
  24. tracelabel/ctf/validation.py +273 -0
  25. tracelabel/db/__init__.py +0 -0
  26. tracelabel/db/annotations.py +204 -0
  27. tracelabel/db/database.py +101 -0
  28. tracelabel/db/locking.py +100 -0
  29. tracelabel/db/migrations.py +94 -0
  30. tracelabel/db/tasks.py +159 -0
  31. tracelabel/db/traces.py +168 -0
  32. tracelabel/demo_data/traces.jsonl +25 -0
  33. tracelabel/errors.py +17 -0
  34. tracelabel/exporting/__init__.py +0 -0
  35. tracelabel/exporting/serializers.py +71 -0
  36. tracelabel/exporting/service.py +168 -0
  37. tracelabel/imports/__init__.py +0 -0
  38. tracelabel/imports/adapters/__init__.py +0 -0
  39. tracelabel/imports/adapters/adk.py +131 -0
  40. tracelabel/imports/adapters/base.py +65 -0
  41. tracelabel/imports/adapters/ctf.py +24 -0
  42. tracelabel/imports/adapters/datadog.py +149 -0
  43. tracelabel/imports/adapters/documents.py +45 -0
  44. tracelabel/imports/adapters/loose.py +144 -0
  45. tracelabel/imports/parsing.py +178 -0
  46. tracelabel/imports/service.py +113 -0
  47. tracelabel/static/assets/index-Ggdv0yW5.js +68 -0
  48. tracelabel/static/assets/index-XiSIyDmC.css +1 -0
  49. tracelabel/static/index.html +23 -0
  50. tracelabel/suggestions/__init__.py +0 -0
  51. tracelabel/suggestions/client.py +75 -0
  52. tracelabel/suggestions/prompts.py +123 -0
  53. tracelabel/suggestions/service.py +203 -0
  54. tracelabel-0.1.0.dist-info/METADATA +176 -0
  55. tracelabel-0.1.0.dist-info/RECORD +58 -0
  56. tracelabel-0.1.0.dist-info/WHEEL +4 -0
  57. tracelabel-0.1.0.dist-info/entry_points.txt +2 -0
  58. tracelabel-0.1.0.dist-info/licenses/LICENSE +201 -0
tracelabel/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
tracelabel/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from tracelabel.cli.app import run
2
+
3
+ if __name__ == "__main__":
4
+ run()
File without changes
tracelabel/api/app.py ADDED
@@ -0,0 +1,40 @@
1
+ from pathlib import Path
2
+
3
+ from fastapi import FastAPI, Request
4
+ from fastapi.responses import JSONResponse
5
+
6
+ from tracelabel.config.models import ResolvedTaskConfig
7
+ from tracelabel.db.database import Database
8
+ from tracelabel.errors import NotFoundError, UserError
9
+
10
+ from .labeling import LabelingService
11
+ from .routes import create_api_router
12
+ from .static import configure_static, default_static_dir
13
+
14
+
15
+ def create_app(
16
+ database: Database,
17
+ config: ResolvedTaskConfig,
18
+ queue: list[str],
19
+ static_dir: Path | None = None,
20
+ ) -> FastAPI:
21
+ service = LabelingService(
22
+ config,
23
+ queue,
24
+ database.traces,
25
+ database.tasks,
26
+ database.annotations,
27
+ )
28
+ app = FastAPI()
29
+
30
+ @app.exception_handler(NotFoundError)
31
+ async def not_found(_request: Request, error: NotFoundError) -> JSONResponse:
32
+ return JSONResponse(status_code=404, content={"detail": str(error)})
33
+
34
+ @app.exception_handler(UserError)
35
+ async def user_error(_request: Request, error: UserError) -> JSONResponse:
36
+ return JSONResponse(status_code=422, content={"detail": str(error)})
37
+
38
+ app.include_router(create_api_router(service))
39
+ configure_static(app, static_dir or default_static_dir())
40
+ return app
@@ -0,0 +1,202 @@
1
+ import sqlite3
2
+ from typing import Any, cast
3
+
4
+ from tracelabel.config.models import ResolvedTaskConfig
5
+ from tracelabel.config.validation import AnnotationValidator
6
+ from tracelabel.db.annotations import AnnotationRepository
7
+ from tracelabel.db.database import decode_json
8
+ from tracelabel.db.tasks import TaskRepository
9
+ from tracelabel.db.traces import TraceRepository
10
+ from tracelabel.errors import NotFoundError, UserError
11
+
12
+ from .models import (
13
+ AnnotationIn,
14
+ AnnotationOut,
15
+ DocumentOut,
16
+ Progress,
17
+ QueueEntry,
18
+ SessionInfo,
19
+ SuggestionOut,
20
+ TraceDetail,
21
+ TraceInfo,
22
+ TurnOut,
23
+ )
24
+
25
+
26
+ class LabelingService:
27
+ def __init__(
28
+ self,
29
+ config: ResolvedTaskConfig,
30
+ queue: list[str],
31
+ traces: TraceRepository,
32
+ tasks: TaskRepository,
33
+ annotations: AnnotationRepository,
34
+ validator: AnnotationValidator | None = None,
35
+ ) -> None:
36
+ self._config = config
37
+ self._queue = queue
38
+ self._traces = traces
39
+ self._tasks = tasks
40
+ self._annotations = annotations
41
+ self._validator = validator or AnnotationValidator(config.fields)
42
+
43
+ def session(self) -> SessionInfo:
44
+ return SessionInfo(
45
+ task=self._config.name,
46
+ level=self._config.level,
47
+ fields=self._config.fields,
48
+ label_roles=self._config.label_roles,
49
+ annotator=self._config.annotator,
50
+ schema_hash=self._config.schema_hash,
51
+ shuffle=self._config.shuffle,
52
+ )
53
+
54
+ def queue(self) -> list[QueueEntry]:
55
+ counts = self._annotations.target_counts(self._task(), self._config.annotator)
56
+ entries: list[QueueEntry] = []
57
+ for position, trace_id in enumerate(self._queue):
58
+ target_count, labeled_count, skipped_count = counts.get(trace_id, (0, 0, 0))
59
+ entries.append(
60
+ QueueEntry(
61
+ trace_id=trace_id,
62
+ position=position,
63
+ n_targets=target_count,
64
+ n_labeled=labeled_count,
65
+ n_skipped=skipped_count,
66
+ )
67
+ )
68
+ return entries
69
+
70
+ def trace_detail(self, trace_id: str) -> TraceDetail:
71
+ trace = self._traces.get(trace_id)
72
+ if trace is None:
73
+ raise NotFoundError(f"unknown trace '{trace_id}'")
74
+ document = self._document_out(trace)
75
+ turns = (
76
+ [] if document else [self._turn_out(row) for row in self._traces.get_turns(trace_id)]
77
+ )
78
+ annotations = {
79
+ str(row["target_id"]): self._annotation_out(row)
80
+ for row in self._annotations.annotations_for_trace(
81
+ self._config.name,
82
+ self._config.annotator,
83
+ trace_id,
84
+ )
85
+ }
86
+ suggestions = {
87
+ str(row["target_id"]): self._suggestion_out(row)
88
+ for row in self._annotations.suggestions_for_trace(self._config.name, trace_id)
89
+ }
90
+ return TraceDetail(
91
+ trace=TraceInfo(
92
+ id=trace["id"],
93
+ source=trace["source"],
94
+ metadata=self._json_object(trace["metadata"]),
95
+ ),
96
+ turns=turns,
97
+ document=document,
98
+ annotations=annotations,
99
+ suggestions=suggestions,
100
+ )
101
+
102
+ def put_annotation(self, annotation: AnnotationIn) -> AnnotationOut:
103
+ self._validate_target(annotation)
104
+ self._validator.validate(annotation.values, annotation.status)
105
+ row = self._annotations.upsert_annotation(
106
+ task=self._config.name,
107
+ target_type=annotation.target_type,
108
+ target_id=annotation.target_id,
109
+ status=annotation.status,
110
+ values=dict(annotation.values),
111
+ annotator=self._config.annotator,
112
+ schema_hash=self._config.schema_hash,
113
+ prefill_model=annotation.prefill_model,
114
+ )
115
+ return self._annotation_out(row)
116
+
117
+ def progress(self) -> Progress:
118
+ counts = self._annotations.target_counts(self._task(), self._config.annotator)
119
+ return Progress(
120
+ unit="turns" if self._config.level == "turn" else "traces",
121
+ total=sum(count[0] for count in counts.values()),
122
+ labeled=sum(count[1] for count in counts.values()),
123
+ skipped=sum(count[2] for count in counts.values()),
124
+ )
125
+
126
+ def _task(self) -> sqlite3.Row:
127
+ task = self._tasks.get(self._config.name)
128
+ if task is None:
129
+ raise UserError(f"task '{self._config.name}' is not open")
130
+ return task
131
+
132
+ def _validate_target(self, annotation: AnnotationIn) -> None:
133
+ if annotation.target_type != self._config.level:
134
+ raise UserError(
135
+ f"target_type '{annotation.target_type}' must match task level "
136
+ f"'{self._config.level}'"
137
+ )
138
+ if annotation.target_type == "trace":
139
+ if self._traces.get(annotation.target_id) is None:
140
+ raise NotFoundError(f"unknown trace '{annotation.target_id}'")
141
+ return
142
+ turn = self._traces.get_turn(annotation.target_id)
143
+ if turn is None:
144
+ raise NotFoundError(f"unknown turn '{annotation.target_id}'")
145
+ if turn["role"] not in self._config.label_roles:
146
+ raise UserError(
147
+ f"turn '{annotation.target_id}' has role '{turn['role']}', not labelable"
148
+ )
149
+
150
+ @staticmethod
151
+ def _document_out(trace: sqlite3.Row) -> DocumentOut | None:
152
+ content = trace["content"]
153
+ if content is None:
154
+ return None
155
+ return DocumentOut(content=content, content_type=trace["content_type"] or "text")
156
+
157
+ def _turn_out(self, row: sqlite3.Row) -> TurnOut:
158
+ raw_tool_calls = row["tool_calls"]
159
+ tool_calls = (
160
+ cast(list[dict[str, Any]], decode_json(raw_tool_calls))
161
+ if raw_tool_calls is not None
162
+ else None
163
+ )
164
+ return TurnOut(
165
+ id=row["id"],
166
+ idx=row["idx"],
167
+ role=row["role"],
168
+ content=row["content"],
169
+ content_type=row["content_type"],
170
+ tool_calls=tool_calls,
171
+ tool_call_id=row["tool_call_id"],
172
+ name=row["name"],
173
+ labelable=(self._config.level == "turn" and row["role"] in self._config.label_roles),
174
+ metadata=self._json_object(row["metadata"]),
175
+ )
176
+
177
+ @classmethod
178
+ def _annotation_out(cls, row: sqlite3.Row) -> AnnotationOut:
179
+ return AnnotationOut(
180
+ target_type=row["target_type"],
181
+ target_id=row["target_id"],
182
+ status=row["status"],
183
+ values=cls._json_object(row["values"]),
184
+ prefill_model=row["prefill_model"],
185
+ schema_hash=row["schema_hash"],
186
+ annotator=row["annotator"],
187
+ created_at=row["created_at"],
188
+ updated_at=row["updated_at"],
189
+ )
190
+
191
+ @classmethod
192
+ def _suggestion_out(cls, row: sqlite3.Row) -> SuggestionOut:
193
+ return SuggestionOut(
194
+ target_id=row["target_id"],
195
+ values=cls._json_object(row["values"]),
196
+ model=row["model"],
197
+ created_at=row["created_at"],
198
+ )
199
+
200
+ @staticmethod
201
+ def _json_object(raw: str) -> dict[str, Any]:
202
+ return cast(dict[str, Any], decode_json(raw))
@@ -0,0 +1,92 @@
1
+ from typing import Any, Literal
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+ from tracelabel.config.models import AnnotationStatus, Level
6
+ from tracelabel.db.annotations import TargetType
7
+
8
+
9
+ class SessionInfo(BaseModel):
10
+ task: str
11
+ level: Level
12
+ fields: list[dict[str, Any]]
13
+ label_roles: list[str]
14
+ annotator: str
15
+ schema_hash: str
16
+ shuffle: bool
17
+
18
+
19
+ class QueueEntry(BaseModel):
20
+ trace_id: str
21
+ position: int
22
+ n_targets: int
23
+ n_labeled: int
24
+ n_skipped: int
25
+
26
+
27
+ class TraceInfo(BaseModel):
28
+ id: str
29
+ source: str | None = None
30
+ metadata: dict[str, Any]
31
+
32
+
33
+ class DocumentOut(BaseModel):
34
+ content: str
35
+ content_type: Literal["text", "json", "html", "markdown"]
36
+
37
+
38
+ class TurnOut(BaseModel):
39
+ id: str
40
+ idx: int
41
+ role: str
42
+ content: str
43
+ content_type: Literal["text", "json", "html", "parts"]
44
+ tool_calls: list[dict[str, Any]] | None = None
45
+ tool_call_id: str | None = None
46
+ name: str | None = None
47
+ labelable: bool
48
+ metadata: dict[str, Any]
49
+
50
+
51
+ class AnnotationIn(BaseModel):
52
+ model_config = ConfigDict(extra="forbid")
53
+
54
+ target_type: TargetType
55
+ target_id: str
56
+ status: AnnotationStatus
57
+ values: dict[str, str | list[str]]
58
+ prefill_model: str | None = None
59
+
60
+
61
+ class AnnotationOut(BaseModel):
62
+ target_type: TargetType
63
+ target_id: str
64
+ status: AnnotationStatus
65
+ values: dict[str, Any]
66
+ prefill_model: str | None = None
67
+ schema_hash: str
68
+ annotator: str
69
+ created_at: str
70
+ updated_at: str
71
+
72
+
73
+ class SuggestionOut(BaseModel):
74
+ target_id: str
75
+ values: dict[str, Any]
76
+ model: str
77
+ created_at: str
78
+
79
+
80
+ class TraceDetail(BaseModel):
81
+ trace: TraceInfo
82
+ turns: list[TurnOut]
83
+ document: DocumentOut | None = None
84
+ annotations: dict[str, AnnotationOut]
85
+ suggestions: dict[str, SuggestionOut]
86
+
87
+
88
+ class Progress(BaseModel):
89
+ unit: Literal["turns", "traces"]
90
+ total: int
91
+ labeled: int
92
+ skipped: int
@@ -0,0 +1,37 @@
1
+ from fastapi import APIRouter
2
+
3
+ from .labeling import LabelingService
4
+ from .models import (
5
+ AnnotationIn,
6
+ AnnotationOut,
7
+ Progress,
8
+ QueueEntry,
9
+ SessionInfo,
10
+ TraceDetail,
11
+ )
12
+
13
+
14
+ def create_api_router(service: LabelingService) -> APIRouter:
15
+ router = APIRouter(prefix="/api")
16
+
17
+ @router.get("/session", response_model=SessionInfo)
18
+ async def get_session() -> SessionInfo:
19
+ return service.session()
20
+
21
+ @router.get("/queue", response_model=list[QueueEntry])
22
+ async def get_queue() -> list[QueueEntry]:
23
+ return service.queue()
24
+
25
+ @router.get("/traces/{trace_id}", response_model=TraceDetail)
26
+ async def get_trace(trace_id: str) -> TraceDetail:
27
+ return service.trace_detail(trace_id)
28
+
29
+ @router.put("/annotations", response_model=AnnotationOut)
30
+ async def put_annotation(annotation: AnnotationIn) -> AnnotationOut:
31
+ return service.put_annotation(annotation)
32
+
33
+ @router.get("/progress", response_model=Progress)
34
+ async def get_progress() -> Progress:
35
+ return service.progress()
36
+
37
+ return router
@@ -0,0 +1,37 @@
1
+ from pathlib import Path
2
+
3
+ from fastapi import FastAPI, HTTPException
4
+ from fastapi.responses import FileResponse, Response
5
+ from fastapi.staticfiles import StaticFiles
6
+ from starlette.types import Scope
7
+
8
+
9
+ class ImmutableStaticFiles(StaticFiles):
10
+ async def get_response(self, path: str, scope: Scope) -> Response:
11
+ response = await super().get_response(path, scope)
12
+ response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
13
+ return response
14
+
15
+
16
+ def default_static_dir() -> Path:
17
+ return Path(__file__).parent.parent / "static"
18
+
19
+
20
+ def configure_static(app: FastAPI, static_dir: Path) -> None:
21
+ assets_dir = static_dir / "assets"
22
+ if assets_dir.is_dir():
23
+ app.mount("/assets", ImmutableStaticFiles(directory=assets_dir), name="assets")
24
+
25
+ async def spa(full_path: str) -> FileResponse:
26
+ if full_path == "api" or full_path.startswith("api/"):
27
+ raise HTTPException(status_code=404, detail="not found")
28
+ index = static_dir / "index.html"
29
+ if not index.is_file():
30
+ raise HTTPException(
31
+ status_code=503,
32
+ detail="frontend not built — run `npm run build` in frontend/ "
33
+ "or use the Vite dev server",
34
+ )
35
+ return FileResponse(index)
36
+
37
+ app.add_api_route("/{full_path:path}", spa, methods=["GET"])
File without changes
tracelabel/cli/app.py ADDED
@@ -0,0 +1,163 @@
1
+ from pathlib import Path
2
+ from typing import Annotated
3
+
4
+ import click
5
+ import typer
6
+
7
+ from tracelabel.config.loader import raw_config_for_target
8
+ from tracelabel.config.models import CliArgs
9
+ from tracelabel.config.resolver import ConfigResolver
10
+ from tracelabel.errors import TraceLabelError, UserError
11
+
12
+ from .commands import (
13
+ DemoCommand,
14
+ ExportCommand,
15
+ ImportCommand,
16
+ ServeCommand,
17
+ SuggestCommand,
18
+ TasksListCommand,
19
+ )
20
+ from .options import (
21
+ FormatChoice,
22
+ FromChoice,
23
+ LevelChoice,
24
+ OnConflictChoice,
25
+ StatusChoice,
26
+ )
27
+
28
+ app = typer.Typer(add_completion=False)
29
+ tasks_app = typer.Typer(add_completion=False)
30
+ app.add_typer(tasks_app, name="tasks")
31
+
32
+
33
+ @app.callback()
34
+ def main() -> None:
35
+ pass
36
+
37
+
38
+ def target_path(target: str | None) -> Path:
39
+ if target is not None:
40
+ return Path(target)
41
+ fallback = Path("config.yaml")
42
+ if fallback.exists():
43
+ return fallback
44
+ raise UserError("No data file given (arg or `data:` in YAML)")
45
+
46
+
47
+ @app.command()
48
+ def serve(
49
+ target: Annotated[str | None, typer.Argument()] = None,
50
+ task: Annotated[str | None, typer.Option("--task")] = None,
51
+ level: Annotated[LevelChoice | None, typer.Option("--level")] = None,
52
+ annotator: Annotated[str | None, typer.Option("--annotator")] = None,
53
+ shuffle: Annotated[bool | None, typer.Option("--shuffle/--no-shuffle")] = None,
54
+ db: Annotated[Path | None, typer.Option("--db")] = None,
55
+ port: Annotated[int, typer.Option("--port")] = 8377,
56
+ no_browser: Annotated[bool, typer.Option("--no-browser")] = False,
57
+ yes: Annotated[bool, typer.Option("--yes")] = False,
58
+ ) -> None:
59
+ path = target_path(target)
60
+ cli = CliArgs(
61
+ task=task,
62
+ level=level.value if level else None,
63
+ annotator=annotator,
64
+ shuffle=shuffle,
65
+ db=db,
66
+ yes=yes,
67
+ )
68
+ config = ConfigResolver().resolve(raw_config_for_target(path), cli)
69
+ project_dir = path if path.is_dir() else path.parent
70
+ ServeCommand().execute(config, project_dir, db, port, no_browser, yes)
71
+
72
+
73
+ @app.command(name="import")
74
+ def import_(
75
+ target: Annotated[str, typer.Argument()],
76
+ from_: Annotated[FromChoice, typer.Option("--from")] = FromChoice.auto,
77
+ db: Annotated[Path | None, typer.Option("--db")] = None,
78
+ on_conflict: Annotated[
79
+ OnConflictChoice,
80
+ typer.Option("--on-conflict"),
81
+ ] = OnConflictChoice.fail,
82
+ skip_invalid: Annotated[bool, typer.Option("--skip-invalid")] = False,
83
+ as_documents: Annotated[bool, typer.Option("--as-documents")] = False,
84
+ ) -> None:
85
+ ImportCommand().execute(
86
+ Path(target),
87
+ database_path=db,
88
+ from_=from_.value,
89
+ on_conflict=on_conflict.value,
90
+ skip_invalid=skip_invalid,
91
+ as_documents=as_documents,
92
+ )
93
+
94
+
95
+ @app.command()
96
+ def export(
97
+ task: Annotated[str | None, typer.Option("--task")] = None,
98
+ db: Annotated[Path | None, typer.Option("--db")] = None,
99
+ format: Annotated[FormatChoice, typer.Option("--format")] = FormatChoice.jsonl,
100
+ joined: Annotated[bool, typer.Option("--joined")] = False,
101
+ out: Annotated[str | None, typer.Option("--out")] = None,
102
+ status: Annotated[StatusChoice, typer.Option("--status")] = StatusChoice.all,
103
+ ) -> None:
104
+ ExportCommand().execute(
105
+ task=task,
106
+ database_path=db,
107
+ format=format.value,
108
+ joined=joined,
109
+ out=Path(out) if out is not None else None,
110
+ status=status.value,
111
+ )
112
+
113
+
114
+ @tasks_app.command("list")
115
+ def tasks_list(db: Annotated[Path | None, typer.Option("--db")] = None) -> None:
116
+ TasksListCommand().execute(db)
117
+
118
+
119
+ @app.command()
120
+ def suggest(
121
+ target: Annotated[str | None, typer.Argument()] = None,
122
+ task: Annotated[str | None, typer.Option("--task")] = None,
123
+ db: Annotated[Path | None, typer.Option("--db")] = None,
124
+ limit: Annotated[int | None, typer.Option("--limit")] = None,
125
+ overwrite: Annotated[bool, typer.Option("--overwrite")] = False,
126
+ concurrency: Annotated[int, typer.Option("--concurrency")] = 4,
127
+ ) -> None:
128
+ path = target_path(target)
129
+ config = ConfigResolver().resolve(
130
+ raw_config_for_target(path),
131
+ CliArgs(task=task, db=db),
132
+ )
133
+ summary = SuggestCommand().execute(
134
+ config,
135
+ path.parent,
136
+ db,
137
+ limit=limit,
138
+ overwrite=overwrite,
139
+ concurrency=concurrency,
140
+ )
141
+ attempted = summary.ok + summary.failed
142
+ typer.echo(f"suggested {summary.ok}/{attempted} · {summary.failed} failed (re-run to retry)")
143
+
144
+
145
+ @app.command()
146
+ def demo(
147
+ port: Annotated[int, typer.Option("--port")] = 8377,
148
+ no_browser: Annotated[bool, typer.Option("--no-browser")] = False,
149
+ ) -> None:
150
+ DemoCommand().execute(port, no_browser)
151
+
152
+
153
+ def run() -> None:
154
+ try:
155
+ app(standalone_mode=False)
156
+ except TraceLabelError as error:
157
+ typer.echo(str(error), err=True)
158
+ raise SystemExit(error.exit_code) from error
159
+ except (click.exceptions.Abort, KeyboardInterrupt) as error:
160
+ raise SystemExit(130) from error
161
+ except click.exceptions.ClickException as error:
162
+ error.show()
163
+ raise SystemExit(error.exit_code) from error