lightly-studio 0.3.1__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.
Potentially problematic release.
This version of lightly-studio might be problematic. Click here for more details.
- lightly_studio/__init__.py +11 -0
- lightly_studio/api/__init__.py +0 -0
- lightly_studio/api/app.py +110 -0
- lightly_studio/api/cache.py +77 -0
- lightly_studio/api/db.py +133 -0
- lightly_studio/api/db_tables.py +32 -0
- lightly_studio/api/features.py +7 -0
- lightly_studio/api/routes/api/annotation.py +233 -0
- lightly_studio/api/routes/api/annotation_label.py +90 -0
- lightly_studio/api/routes/api/annotation_task.py +38 -0
- lightly_studio/api/routes/api/classifier.py +387 -0
- lightly_studio/api/routes/api/dataset.py +182 -0
- lightly_studio/api/routes/api/dataset_tag.py +257 -0
- lightly_studio/api/routes/api/exceptions.py +96 -0
- lightly_studio/api/routes/api/features.py +17 -0
- lightly_studio/api/routes/api/metadata.py +37 -0
- lightly_studio/api/routes/api/metrics.py +80 -0
- lightly_studio/api/routes/api/sample.py +196 -0
- lightly_studio/api/routes/api/settings.py +45 -0
- lightly_studio/api/routes/api/status.py +19 -0
- lightly_studio/api/routes/api/text_embedding.py +48 -0
- lightly_studio/api/routes/api/validators.py +17 -0
- lightly_studio/api/routes/healthz.py +13 -0
- lightly_studio/api/routes/images.py +104 -0
- lightly_studio/api/routes/webapp.py +51 -0
- lightly_studio/api/server.py +82 -0
- lightly_studio/core/__init__.py +0 -0
- lightly_studio/core/dataset.py +523 -0
- lightly_studio/core/sample.py +77 -0
- lightly_studio/core/start_gui.py +15 -0
- lightly_studio/dataset/__init__.py +0 -0
- lightly_studio/dataset/edge_embedding_generator.py +144 -0
- lightly_studio/dataset/embedding_generator.py +91 -0
- lightly_studio/dataset/embedding_manager.py +163 -0
- lightly_studio/dataset/env.py +16 -0
- lightly_studio/dataset/file_utils.py +35 -0
- lightly_studio/dataset/loader.py +622 -0
- lightly_studio/dataset/mobileclip_embedding_generator.py +144 -0
- lightly_studio/dist_lightly_studio_view_app/_app/env.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.DenzbfeK.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/LightlyLogo.BNjCIww-.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans- +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Bold.DGvYQtcs.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Italic-VariableFont_wdth_wght.B4AZ-wl6.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Regular.DxJTClRG.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-SemiBold.D3TTYgdB.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-VariableFont_wdth_wght.BZBpG5Iz.ttf +0 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.OwPEPQZu.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.b653GmVf.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.T-zjSUd3.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/useFeatureFlags.CV-KWLNP.css +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/69_IOA4Y.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B2FVR0s0.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B90CZVMX.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B9zumHo5.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BJXwVxaE.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bsi3UGy5.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bu7uvVrG.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bx1xMsFy.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BylOuP6i.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C8I8rFJQ.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CDnpyLsT.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWj6FrbW.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CYgJF_JY.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CcaPhhk3.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvOmgdoc.js +93 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxtLVaYz.js +3 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D5-A_Ffd.js +4 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6RI2Zrd.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6su9Aln.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D98V7j6A.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIRAtgl0.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIeogL5L.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DOlTMNyt.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjUWrjOv.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjfY96ND.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/H7C68rOM.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/O-EABkf9.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/XO7A28GO.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/hQVEETDE.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/l7KrR96u.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/nAHhluT7.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/r64xT6ao.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/vC4nQVEB.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/x9G_hzyY.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.CjnvpsmS.js +2 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.0o1H7wM9.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.XRq_TUwu.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/1.B4rNYwVp.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.DfBwOEhN.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/11.CWG1ehzT.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CwF2_8mP.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.CS4muRY-.js +6 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/3.CWHpKonm.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/4.OUWOLQeV.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.Dm6t9F5W.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.Bw5ck4gK.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.CF0EDTR6.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cw30LEcV.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/9.CPu3CiBc.js +1 -0
- lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -0
- lightly_studio/dist_lightly_studio_view_app/apple-touch-icon-precomposed.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/apple-touch-icon.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/favicon.png +0 -0
- lightly_studio/dist_lightly_studio_view_app/index.html +44 -0
- lightly_studio/examples/example.py +23 -0
- lightly_studio/examples/example_metadata.py +338 -0
- lightly_studio/examples/example_selection.py +39 -0
- lightly_studio/examples/example_split_work.py +67 -0
- lightly_studio/examples/example_v2.py +21 -0
- lightly_studio/export_schema.py +18 -0
- lightly_studio/few_shot_classifier/__init__.py +0 -0
- lightly_studio/few_shot_classifier/classifier.py +80 -0
- lightly_studio/few_shot_classifier/classifier_manager.py +663 -0
- lightly_studio/few_shot_classifier/random_forest_classifier.py +489 -0
- lightly_studio/metadata/complex_metadata.py +47 -0
- lightly_studio/metadata/gps_coordinate.py +41 -0
- lightly_studio/metadata/metadata_protocol.py +17 -0
- lightly_studio/metrics/__init__.py +0 -0
- lightly_studio/metrics/detection/__init__.py +0 -0
- lightly_studio/metrics/detection/map.py +268 -0
- lightly_studio/models/__init__.py +1 -0
- lightly_studio/models/annotation/__init__.py +0 -0
- lightly_studio/models/annotation/annotation_base.py +171 -0
- lightly_studio/models/annotation/instance_segmentation.py +56 -0
- lightly_studio/models/annotation/links.py +17 -0
- lightly_studio/models/annotation/object_detection.py +47 -0
- lightly_studio/models/annotation/semantic_segmentation.py +44 -0
- lightly_studio/models/annotation_label.py +47 -0
- lightly_studio/models/annotation_task.py +28 -0
- lightly_studio/models/classifier.py +20 -0
- lightly_studio/models/dataset.py +84 -0
- lightly_studio/models/embedding_model.py +30 -0
- lightly_studio/models/metadata.py +208 -0
- lightly_studio/models/sample.py +180 -0
- lightly_studio/models/sample_embedding.py +37 -0
- lightly_studio/models/settings.py +60 -0
- lightly_studio/models/tag.py +96 -0
- lightly_studio/py.typed +0 -0
- lightly_studio/resolvers/__init__.py +7 -0
- lightly_studio/resolvers/annotation_label_resolver/__init__.py +21 -0
- lightly_studio/resolvers/annotation_label_resolver/create.py +27 -0
- lightly_studio/resolvers/annotation_label_resolver/delete.py +28 -0
- lightly_studio/resolvers/annotation_label_resolver/get_all.py +22 -0
- lightly_studio/resolvers/annotation_label_resolver/get_by_id.py +24 -0
- lightly_studio/resolvers/annotation_label_resolver/get_by_ids.py +25 -0
- lightly_studio/resolvers/annotation_label_resolver/get_by_label_name.py +24 -0
- lightly_studio/resolvers/annotation_label_resolver/names_by_ids.py +25 -0
- lightly_studio/resolvers/annotation_label_resolver/update.py +38 -0
- lightly_studio/resolvers/annotation_resolver/__init__.py +33 -0
- lightly_studio/resolvers/annotation_resolver/count_annotations_by_dataset.py +120 -0
- lightly_studio/resolvers/annotation_resolver/create.py +19 -0
- lightly_studio/resolvers/annotation_resolver/create_many.py +96 -0
- lightly_studio/resolvers/annotation_resolver/delete_annotation.py +45 -0
- lightly_studio/resolvers/annotation_resolver/delete_annotations.py +56 -0
- lightly_studio/resolvers/annotation_resolver/get_all.py +74 -0
- lightly_studio/resolvers/annotation_resolver/get_by_id.py +18 -0
- lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +144 -0
- lightly_studio/resolvers/annotation_resolver/update_bounding_box.py +68 -0
- lightly_studio/resolvers/annotation_task_resolver.py +31 -0
- lightly_studio/resolvers/annotations/__init__.py +1 -0
- lightly_studio/resolvers/annotations/annotations_filter.py +89 -0
- lightly_studio/resolvers/dataset_resolver.py +278 -0
- lightly_studio/resolvers/embedding_model_resolver.py +100 -0
- lightly_studio/resolvers/metadata_resolver/__init__.py +15 -0
- lightly_studio/resolvers/metadata_resolver/metadata_filter.py +163 -0
- lightly_studio/resolvers/metadata_resolver/sample/__init__.py +21 -0
- lightly_studio/resolvers/metadata_resolver/sample/bulk_set_metadata.py +48 -0
- lightly_studio/resolvers/metadata_resolver/sample/get_by_sample_id.py +24 -0
- lightly_studio/resolvers/metadata_resolver/sample/get_metadata_info.py +104 -0
- lightly_studio/resolvers/metadata_resolver/sample/get_value_for_sample.py +27 -0
- lightly_studio/resolvers/metadata_resolver/sample/set_value_for_sample.py +53 -0
- lightly_studio/resolvers/sample_embedding_resolver.py +86 -0
- lightly_studio/resolvers/sample_resolver.py +249 -0
- lightly_studio/resolvers/samples_filter.py +81 -0
- lightly_studio/resolvers/settings_resolver.py +58 -0
- lightly_studio/resolvers/tag_resolver.py +276 -0
- lightly_studio/selection/README.md +6 -0
- lightly_studio/selection/mundig.py +105 -0
- lightly_studio/selection/select.py +96 -0
- lightly_studio/selection/select_via_db.py +93 -0
- lightly_studio/selection/selection_config.py +31 -0
- lightly_studio/services/annotations_service/__init__.py +21 -0
- lightly_studio/services/annotations_service/get_annotation_by_id.py +31 -0
- lightly_studio/services/annotations_service/update_annotation.py +65 -0
- lightly_studio/services/annotations_service/update_annotation_label.py +48 -0
- lightly_studio/services/annotations_service/update_annotations.py +29 -0
- lightly_studio/setup_logging.py +19 -0
- lightly_studio/type_definitions.py +19 -0
- lightly_studio/vendor/ACKNOWLEDGEMENTS +422 -0
- lightly_studio/vendor/LICENSE +31 -0
- lightly_studio/vendor/LICENSE_weights_data +50 -0
- lightly_studio/vendor/README.md +5 -0
- lightly_studio/vendor/__init__.py +1 -0
- lightly_studio/vendor/mobileclip/__init__.py +96 -0
- lightly_studio/vendor/mobileclip/clip.py +77 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_b.json +18 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_s0.json +18 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_s1.json +18 -0
- lightly_studio/vendor/mobileclip/configs/mobileclip_s2.json +18 -0
- lightly_studio/vendor/mobileclip/image_encoder.py +67 -0
- lightly_studio/vendor/mobileclip/logger.py +154 -0
- lightly_studio/vendor/mobileclip/models/__init__.py +10 -0
- lightly_studio/vendor/mobileclip/models/mci.py +933 -0
- lightly_studio/vendor/mobileclip/models/vit.py +433 -0
- lightly_studio/vendor/mobileclip/modules/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/common/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/common/mobileone.py +341 -0
- lightly_studio/vendor/mobileclip/modules/common/transformer.py +451 -0
- lightly_studio/vendor/mobileclip/modules/image/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/image/image_projection.py +113 -0
- lightly_studio/vendor/mobileclip/modules/image/replknet.py +188 -0
- lightly_studio/vendor/mobileclip/modules/text/__init__.py +4 -0
- lightly_studio/vendor/mobileclip/modules/text/repmixer.py +281 -0
- lightly_studio/vendor/mobileclip/modules/text/tokenizer.py +38 -0
- lightly_studio/vendor/mobileclip/text_encoder.py +245 -0
- lightly_studio-0.3.1.dist-info/METADATA +520 -0
- lightly_studio-0.3.1.dist-info/RECORD +219 -0
- lightly_studio-0.3.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Set up logging before importing any other modules.
|
|
2
|
+
# Add noqa to silence unused import and unsorted imports linter warnings.
|
|
3
|
+
from . import setup_logging # noqa: F401 I001
|
|
4
|
+
|
|
5
|
+
# TODO (Jonas 08/25): This will be removed as soon as the new interface is used in the examples
|
|
6
|
+
from lightly_studio.dataset.loader import DatasetLoader
|
|
7
|
+
from lightly_studio.core.dataset import Dataset
|
|
8
|
+
from lightly_studio.core.start_gui import start_gui
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = ["Dataset", "DatasetLoader", "start_gui"]
|
|
File without changes
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""This module contains the FastAPI app configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, FastAPI
|
|
9
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
from fastapi.routing import APIRoute
|
|
11
|
+
from sqlmodel import Session
|
|
12
|
+
from typing_extensions import Annotated
|
|
13
|
+
|
|
14
|
+
from lightly_studio.api.db import db_manager
|
|
15
|
+
from lightly_studio.api.routes import healthz, images, webapp
|
|
16
|
+
from lightly_studio.api.routes.api import (
|
|
17
|
+
annotation,
|
|
18
|
+
annotation_label,
|
|
19
|
+
annotation_task,
|
|
20
|
+
classifier,
|
|
21
|
+
dataset,
|
|
22
|
+
dataset_tag,
|
|
23
|
+
features,
|
|
24
|
+
metadata,
|
|
25
|
+
metrics,
|
|
26
|
+
sample,
|
|
27
|
+
settings,
|
|
28
|
+
text_embedding,
|
|
29
|
+
)
|
|
30
|
+
from lightly_studio.api.routes.api.exceptions import (
|
|
31
|
+
register_exception_handlers,
|
|
32
|
+
)
|
|
33
|
+
from lightly_studio.dataset.env import LIGHTLY_STUDIO_DEBUG
|
|
34
|
+
|
|
35
|
+
SessionDep = Annotated[Session, Depends(db_manager.session)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@asynccontextmanager
|
|
39
|
+
async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
|
40
|
+
"""Lifespan context for initializing and cleaning up resources.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
_: The FastAPI application instance.
|
|
44
|
+
|
|
45
|
+
Yields:
|
|
46
|
+
None when the application is ready.
|
|
47
|
+
"""
|
|
48
|
+
yield
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if LIGHTLY_STUDIO_DEBUG:
|
|
52
|
+
import logging
|
|
53
|
+
|
|
54
|
+
logging.basicConfig()
|
|
55
|
+
logger = logging.getLogger("sqlalchemy.engine")
|
|
56
|
+
logger.setLevel(logging.DEBUG)
|
|
57
|
+
|
|
58
|
+
"""Create the FastAPI app."""
|
|
59
|
+
app = FastAPI(lifespan=lifespan)
|
|
60
|
+
|
|
61
|
+
app.add_middleware(
|
|
62
|
+
CORSMiddleware,
|
|
63
|
+
allow_origins=["*"],
|
|
64
|
+
allow_credentials=True,
|
|
65
|
+
allow_methods=["*"],
|
|
66
|
+
allow_headers=["*"],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def use_route_names_as_operation_ids(app: FastAPI) -> None:
|
|
71
|
+
"""Use API function name for operation IDs.
|
|
72
|
+
|
|
73
|
+
Should be called only after all routes have been added.
|
|
74
|
+
"""
|
|
75
|
+
for route in app.routes:
|
|
76
|
+
if isinstance(route, APIRoute):
|
|
77
|
+
route.operation_id = route.name # in this case, 'read_items'
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
register_exception_handlers(app)
|
|
81
|
+
|
|
82
|
+
# api routes
|
|
83
|
+
api_router = APIRouter(prefix="/api", tags=["api"])
|
|
84
|
+
|
|
85
|
+
api_router.include_router(dataset.dataset_router)
|
|
86
|
+
api_router.include_router(dataset_tag.tag_router)
|
|
87
|
+
api_router.include_router(sample.samples_router)
|
|
88
|
+
api_router.include_router(annotation_label.annotations_label_router)
|
|
89
|
+
api_router.include_router(annotation.annotations_router)
|
|
90
|
+
api_router.include_router(text_embedding.text_embedding_router)
|
|
91
|
+
api_router.include_router(annotation_task.router)
|
|
92
|
+
api_router.include_router(settings.settings_router)
|
|
93
|
+
api_router.include_router(classifier.classifier_router)
|
|
94
|
+
api_router.include_router(features.features_router)
|
|
95
|
+
api_router.include_router(metadata.metadata_router)
|
|
96
|
+
api_router.include_router(metrics.metrics_router)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
app.include_router(api_router)
|
|
100
|
+
# images serving
|
|
101
|
+
app.include_router(images.app_router, prefix="/images")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# health status check
|
|
105
|
+
app.include_router(healthz.health_router)
|
|
106
|
+
|
|
107
|
+
# webapp routes
|
|
108
|
+
app.include_router(webapp.app_router)
|
|
109
|
+
|
|
110
|
+
use_route_names_as_operation_ids(app)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""This module contains the FastAPI cache configuration for static files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
from os import PathLike, stat_result
|
|
7
|
+
|
|
8
|
+
from fastapi import Response
|
|
9
|
+
from fastapi.staticfiles import StaticFiles
|
|
10
|
+
from starlette.types import Scope
|
|
11
|
+
|
|
12
|
+
from .routes.api.status import HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StaticFilesCache(StaticFiles):
|
|
16
|
+
"""StaticFiles class with cache headers."""
|
|
17
|
+
|
|
18
|
+
days_to_expire = 1
|
|
19
|
+
|
|
20
|
+
def __init__( # noqa: PLR0913 (too-many-arguments)
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
directory: str | PathLike[str] | None = None,
|
|
24
|
+
packages: list[str | tuple[str, str]] | None = None,
|
|
25
|
+
html: bool = False,
|
|
26
|
+
check_dir: bool = True,
|
|
27
|
+
follow_symlink: bool = False,
|
|
28
|
+
cachecontrol: str | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Initialize the StaticFilesCache class."""
|
|
31
|
+
self.cachecontrol = cachecontrol or f"private, max-age={self.days_to_expire * 24 * 60 * 60}"
|
|
32
|
+
super().__init__(
|
|
33
|
+
directory=directory,
|
|
34
|
+
packages=packages,
|
|
35
|
+
html=html,
|
|
36
|
+
check_dir=check_dir,
|
|
37
|
+
follow_symlink=follow_symlink,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def file_response(
|
|
41
|
+
self,
|
|
42
|
+
full_path: str | PathLike[str],
|
|
43
|
+
stat_result: stat_result,
|
|
44
|
+
scope: Scope,
|
|
45
|
+
status_code: int = 200,
|
|
46
|
+
) -> Response:
|
|
47
|
+
"""Override the file_response method to add cache headers."""
|
|
48
|
+
allowed_extensions = (
|
|
49
|
+
# Images
|
|
50
|
+
".png",
|
|
51
|
+
".jpg",
|
|
52
|
+
".jpeg",
|
|
53
|
+
".gif",
|
|
54
|
+
".webp",
|
|
55
|
+
".bmp",
|
|
56
|
+
".tiff",
|
|
57
|
+
# Movies
|
|
58
|
+
".mov",
|
|
59
|
+
".mp4",
|
|
60
|
+
".avi",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if not str(full_path).lower().endswith(allowed_extensions):
|
|
64
|
+
return Response(
|
|
65
|
+
status_code=HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE
|
|
66
|
+
) # Unsupported Media Type
|
|
67
|
+
resp: Response = super().file_response(full_path, stat_result, scope, status_code)
|
|
68
|
+
resp.headers.setdefault("Cache-Control", self.cachecontrol)
|
|
69
|
+
|
|
70
|
+
# Calculate expiration date
|
|
71
|
+
expire_date = datetime.now(timezone.utc) + timedelta(days=self.days_to_expire)
|
|
72
|
+
resp.headers.setdefault("Expires", expire_date.strftime("%a, %d %b %Y %H:%M:%S GMT"))
|
|
73
|
+
|
|
74
|
+
# Add Vary header to make sure caches respect the query parameters
|
|
75
|
+
resp.headers.setdefault("Vary", "Accept-Encoding, Origin, v")
|
|
76
|
+
|
|
77
|
+
return resp
|
lightly_studio/api/db.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Module provides functions to initialize and manage the DuckDB."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from typing import Generator
|
|
9
|
+
|
|
10
|
+
from sqlalchemy.engine import Engine
|
|
11
|
+
from sqlmodel import Session, SQLModel, create_engine
|
|
12
|
+
|
|
13
|
+
import lightly_studio.api.db_tables # noqa: F401, required for SQLModel to work properly
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MockDatabaseManager:
|
|
17
|
+
"""Mock version of DatabaseManager."""
|
|
18
|
+
|
|
19
|
+
_persistent_session: Session | None = None
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
"""Create a new instance of the MockDatabaseManager."""
|
|
23
|
+
self.engine = create_engine("duckdb:///:memory:")
|
|
24
|
+
self._session_instance = Session(self.engine, close_resets_only=False)
|
|
25
|
+
# Initialize tables
|
|
26
|
+
SQLModel.metadata.create_all(self.engine)
|
|
27
|
+
|
|
28
|
+
@contextmanager
|
|
29
|
+
def session(self) -> Generator[Session, None, None]:
|
|
30
|
+
"""Return the database session."""
|
|
31
|
+
try:
|
|
32
|
+
yield self._session_instance
|
|
33
|
+
if not self._session_instance.in_transaction():
|
|
34
|
+
self._session_instance.commit()
|
|
35
|
+
except Exception:
|
|
36
|
+
self._session_instance.rollback()
|
|
37
|
+
raise
|
|
38
|
+
finally:
|
|
39
|
+
self._session_instance.close()
|
|
40
|
+
|
|
41
|
+
def persistent_session(self) -> Session:
|
|
42
|
+
"""Create a persistent session."""
|
|
43
|
+
if self._persistent_session is None:
|
|
44
|
+
self._persistent_session = Session(self.engine, close_resets_only=False)
|
|
45
|
+
return self._persistent_session
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DatabaseManager:
|
|
49
|
+
"""Manages database connections and ensures proper resource handling."""
|
|
50
|
+
|
|
51
|
+
_instance: DatabaseManager | None = None
|
|
52
|
+
engine: Engine | None = None
|
|
53
|
+
_persistent_session: Session | None = None
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def database_exists(db_file: str) -> bool:
|
|
57
|
+
"""Check if database file exists.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
db_file: Path to the database file
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if database file exists, False otherwise
|
|
64
|
+
"""
|
|
65
|
+
return os.path.exists(db_file)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def cleanup_database(db_file: str) -> None:
|
|
69
|
+
"""Delete database file if it exists.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
db_file: Path to the database file to delete
|
|
73
|
+
"""
|
|
74
|
+
if DatabaseManager.database_exists(db_file):
|
|
75
|
+
os.remove(db_file)
|
|
76
|
+
logging.info(f"Deleted existing database: {db_file}")
|
|
77
|
+
|
|
78
|
+
def __new__(
|
|
79
|
+
cls, db_file: str = "lightly_studio.db", cleanup_existing: bool = False
|
|
80
|
+
) -> DatabaseManager:
|
|
81
|
+
"""Create a new instance of the DatabaseManager.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
db_file: Path to the database file
|
|
85
|
+
cleanup_existing: If True, deletes existing database
|
|
86
|
+
before creating a new one
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
DatabaseManager instance
|
|
90
|
+
"""
|
|
91
|
+
if cleanup_existing:
|
|
92
|
+
cls.cleanup_database(db_file)
|
|
93
|
+
|
|
94
|
+
if cls._instance is None:
|
|
95
|
+
cls._instance = super().__new__(cls)
|
|
96
|
+
# File-based DuckDB
|
|
97
|
+
cls._instance.engine = create_engine(f"duckdb:///{db_file}")
|
|
98
|
+
# Initialize tables
|
|
99
|
+
SQLModel.metadata.create_all(cls._instance.engine)
|
|
100
|
+
return cls._instance
|
|
101
|
+
|
|
102
|
+
@contextmanager
|
|
103
|
+
def session(self) -> Generator[Session, None, None]:
|
|
104
|
+
"""Create a new database session."""
|
|
105
|
+
session = Session(self.engine, close_resets_only=False)
|
|
106
|
+
try:
|
|
107
|
+
yield session
|
|
108
|
+
session.commit()
|
|
109
|
+
except Exception:
|
|
110
|
+
session.rollback()
|
|
111
|
+
raise
|
|
112
|
+
finally:
|
|
113
|
+
session.close()
|
|
114
|
+
|
|
115
|
+
def persistent_session(self) -> Session:
|
|
116
|
+
"""Create a persistent session."""
|
|
117
|
+
if self._persistent_session is None:
|
|
118
|
+
# TODO(Michal, 08/2025): Consider setting close_resets_only=True
|
|
119
|
+
# before the release. The current solution is more strict and prevents
|
|
120
|
+
# bugs but it might be too restrictive for users.
|
|
121
|
+
self._persistent_session = Session(self.engine, close_resets_only=False)
|
|
122
|
+
return self._persistent_session
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Global instance
|
|
126
|
+
db_manager = DatabaseManager(cleanup_existing=True)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# For FastAPI dependency injection
|
|
130
|
+
def get_session() -> Generator[Session, None, None]:
|
|
131
|
+
"""Yield a new session for database operations."""
|
|
132
|
+
with db_manager.session() as session:
|
|
133
|
+
yield session
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Module provides functions to initialize and manage the DuckDB."""
|
|
2
|
+
|
|
3
|
+
from lightly_studio.models.annotation.annotation_base import (
|
|
4
|
+
AnnotationBaseTable, # noqa: F401, required for SQLModel to work properly
|
|
5
|
+
)
|
|
6
|
+
from lightly_studio.models.annotation_label import (
|
|
7
|
+
AnnotationLabelTable, # noqa: F401, required for SQLModel to work properly
|
|
8
|
+
)
|
|
9
|
+
from lightly_studio.models.annotation_task import (
|
|
10
|
+
AnnotationTaskTable, # noqa: F401, required for SQLModel to work properly
|
|
11
|
+
)
|
|
12
|
+
from lightly_studio.models.dataset import (
|
|
13
|
+
DatasetTable, # noqa: F401, required for SQLModel to work properly
|
|
14
|
+
)
|
|
15
|
+
from lightly_studio.models.embedding_model import (
|
|
16
|
+
EmbeddingModelTable, # noqa: F401, required for SQLModel to work properly
|
|
17
|
+
)
|
|
18
|
+
from lightly_studio.models.metadata import (
|
|
19
|
+
SampleMetadataTable, # noqa: F401, required for SQLModel to work properly
|
|
20
|
+
)
|
|
21
|
+
from lightly_studio.models.sample import (
|
|
22
|
+
SampleTable, # noqa: F401, required for SQLModel to work properly
|
|
23
|
+
)
|
|
24
|
+
from lightly_studio.models.sample_embedding import (
|
|
25
|
+
SampleEmbeddingTable, # noqa: F401, required for SQLModel to work properly
|
|
26
|
+
)
|
|
27
|
+
from lightly_studio.models.settings import (
|
|
28
|
+
SettingTable, # noqa: F401, required for SQLModel to work properly
|
|
29
|
+
)
|
|
30
|
+
from lightly_studio.models.tag import (
|
|
31
|
+
TagTable, # noqa: F401, required for SQLModel to work properly
|
|
32
|
+
)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""This module contains the API routes for managing annotations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Body, Depends, HTTPException, Path
|
|
8
|
+
from fastapi.params import Query
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from sqlmodel import Session
|
|
11
|
+
from typing_extensions import Annotated
|
|
12
|
+
|
|
13
|
+
from lightly_studio.api.db import get_session
|
|
14
|
+
from lightly_studio.api.routes.api.dataset import get_and_validate_dataset_id
|
|
15
|
+
from lightly_studio.api.routes.api.status import (
|
|
16
|
+
HTTP_STATUS_CREATED,
|
|
17
|
+
HTTP_STATUS_NOT_FOUND,
|
|
18
|
+
)
|
|
19
|
+
from lightly_studio.api.routes.api.validators import Paginated, PaginatedWithCursor
|
|
20
|
+
from lightly_studio.models.annotation.annotation_base import (
|
|
21
|
+
AnnotationBaseTable,
|
|
22
|
+
AnnotationDetailsView,
|
|
23
|
+
AnnotationViewsWithCount,
|
|
24
|
+
)
|
|
25
|
+
from lightly_studio.models.dataset import DatasetTable
|
|
26
|
+
from lightly_studio.resolvers import annotation_resolver, tag_resolver
|
|
27
|
+
from lightly_studio.resolvers.annotation_resolver.get_all import (
|
|
28
|
+
GetAllAnnotationsResult,
|
|
29
|
+
)
|
|
30
|
+
from lightly_studio.resolvers.annotations.annotations_filter import (
|
|
31
|
+
AnnotationsFilter,
|
|
32
|
+
)
|
|
33
|
+
from lightly_studio.services import annotations_service
|
|
34
|
+
from lightly_studio.services.annotations_service.update_annotation import (
|
|
35
|
+
AnnotationUpdate,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
annotations_router = APIRouter(prefix="/datasets/{dataset_id}", tags=["annotations"])
|
|
39
|
+
SessionDep = Annotated[Session, Depends(get_session)]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@annotations_router.get("/annotations/count")
|
|
43
|
+
def count_annotations_by_dataset( # noqa: PLR0913 // FIXME: refactor to use proper pydantic
|
|
44
|
+
dataset: Annotated[
|
|
45
|
+
DatasetTable,
|
|
46
|
+
Path(title="Dataset Id"),
|
|
47
|
+
Depends(get_and_validate_dataset_id),
|
|
48
|
+
],
|
|
49
|
+
session: SessionDep,
|
|
50
|
+
filtered_labels: Annotated[list[str] | None, Query()] = None,
|
|
51
|
+
min_width: Annotated[int | None, Query(ge=0)] = None,
|
|
52
|
+
max_width: Annotated[int | None, Query(ge=0)] = None,
|
|
53
|
+
min_height: Annotated[int | None, Query(ge=0)] = None,
|
|
54
|
+
max_height: Annotated[int | None, Query(ge=0)] = None,
|
|
55
|
+
tag_ids: list[UUID] | None = None,
|
|
56
|
+
) -> list[dict[str, str | int]]:
|
|
57
|
+
"""Get annotation counts for a specific dataset.
|
|
58
|
+
|
|
59
|
+
Returns a list of dictionaries with label name and count.
|
|
60
|
+
"""
|
|
61
|
+
counts = annotation_resolver.count_annotations_by_dataset(
|
|
62
|
+
session=session,
|
|
63
|
+
dataset_id=dataset.dataset_id,
|
|
64
|
+
filtered_labels=filtered_labels,
|
|
65
|
+
min_width=min_width,
|
|
66
|
+
max_width=max_width,
|
|
67
|
+
min_height=min_height,
|
|
68
|
+
max_height=max_height,
|
|
69
|
+
tag_ids=tag_ids,
|
|
70
|
+
)
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
"label_name": label_name,
|
|
74
|
+
"current_count": current_count,
|
|
75
|
+
"total_count": total_count,
|
|
76
|
+
}
|
|
77
|
+
for label_name, current_count, total_count in counts
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@annotations_router.get(
|
|
82
|
+
"/annotations",
|
|
83
|
+
response_model=AnnotationViewsWithCount,
|
|
84
|
+
)
|
|
85
|
+
def read_annotations(
|
|
86
|
+
dataset_id: Annotated[UUID, Path(title="Dataset Id", description="The ID of the dataset")],
|
|
87
|
+
session: SessionDep,
|
|
88
|
+
pagination: Annotated[PaginatedWithCursor, Depends()],
|
|
89
|
+
annotation_label_ids: Annotated[list[UUID] | None, Query()] = None,
|
|
90
|
+
tag_ids: Annotated[list[UUID] | None, Query()] = None,
|
|
91
|
+
) -> GetAllAnnotationsResult:
|
|
92
|
+
"""Retrieve a list of annotations from the database."""
|
|
93
|
+
return annotation_resolver.get_all(
|
|
94
|
+
session=session,
|
|
95
|
+
pagination=Paginated(
|
|
96
|
+
offset=pagination.offset,
|
|
97
|
+
limit=pagination.limit,
|
|
98
|
+
),
|
|
99
|
+
filters=AnnotationsFilter(
|
|
100
|
+
dataset_ids=[dataset_id],
|
|
101
|
+
annotation_label_ids=annotation_label_ids,
|
|
102
|
+
annotation_tag_ids=tag_ids,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@annotations_router.post(
|
|
108
|
+
"/annotations/{annotation_id}/tag/{tag_id}",
|
|
109
|
+
status_code=HTTP_STATUS_CREATED,
|
|
110
|
+
)
|
|
111
|
+
def add_tag_to_annotation(
|
|
112
|
+
session: SessionDep,
|
|
113
|
+
annotation_id: UUID,
|
|
114
|
+
tag_id: UUID,
|
|
115
|
+
) -> bool:
|
|
116
|
+
"""Add annotation to a tag."""
|
|
117
|
+
annotation = annotation_resolver.get_by_id(session=session, annotation_id=annotation_id)
|
|
118
|
+
if not annotation:
|
|
119
|
+
raise HTTPException(
|
|
120
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
121
|
+
detail=f"Annotation {annotation_id} not found",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if not tag_resolver.add_tag_to_annotation(
|
|
125
|
+
session=session, tag_id=tag_id, annotation=annotation
|
|
126
|
+
):
|
|
127
|
+
raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail=f"Tag {tag_id} not found")
|
|
128
|
+
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class AnnotationUpdateInput(BaseModel):
|
|
133
|
+
"""API input model for updating an annotation."""
|
|
134
|
+
|
|
135
|
+
annotation_id: UUID
|
|
136
|
+
dataset_id: UUID
|
|
137
|
+
label_name: str | None
|
|
138
|
+
x: int | None = None
|
|
139
|
+
y: int | None = None
|
|
140
|
+
width: int | None = None
|
|
141
|
+
height: int | None = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@annotations_router.put("/annotations/{annotation_id}")
|
|
145
|
+
def update_annotation(
|
|
146
|
+
session: SessionDep,
|
|
147
|
+
dataset_id: Annotated[
|
|
148
|
+
UUID,
|
|
149
|
+
Path(title="Dataset Id"),
|
|
150
|
+
],
|
|
151
|
+
annotation_id: Annotated[
|
|
152
|
+
UUID,
|
|
153
|
+
Path(title="Annotation ID", description="ID of the annotation to update"),
|
|
154
|
+
],
|
|
155
|
+
annotation_update_input: Annotated[AnnotationUpdateInput, Body()],
|
|
156
|
+
) -> AnnotationBaseTable:
|
|
157
|
+
"""Update an existing annotation in the database."""
|
|
158
|
+
return annotations_service.update_annotation(
|
|
159
|
+
session=session,
|
|
160
|
+
annotation_update=AnnotationUpdate(
|
|
161
|
+
annotation_id=annotation_id,
|
|
162
|
+
dataset_id=dataset_id,
|
|
163
|
+
label_name=annotation_update_input.label_name,
|
|
164
|
+
x=annotation_update_input.x,
|
|
165
|
+
y=annotation_update_input.y,
|
|
166
|
+
width=annotation_update_input.width,
|
|
167
|
+
height=annotation_update_input.height,
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@annotations_router.put(
|
|
173
|
+
"/annotations",
|
|
174
|
+
)
|
|
175
|
+
def update_annotations(
|
|
176
|
+
session: SessionDep,
|
|
177
|
+
dataset_id: Annotated[
|
|
178
|
+
UUID,
|
|
179
|
+
Path(title="Dataset Id"),
|
|
180
|
+
],
|
|
181
|
+
annotation_update_inputs: Annotated[list[AnnotationUpdateInput], Body()],
|
|
182
|
+
) -> list[AnnotationBaseTable]:
|
|
183
|
+
"""Update multiple annotations in the database."""
|
|
184
|
+
return annotations_service.update_annotations(
|
|
185
|
+
session=session,
|
|
186
|
+
annotation_updates=[
|
|
187
|
+
AnnotationUpdate(
|
|
188
|
+
annotation_id=annotation_update_input.annotation_id,
|
|
189
|
+
dataset_id=dataset_id,
|
|
190
|
+
label_name=annotation_update_input.label_name,
|
|
191
|
+
x=annotation_update_input.x,
|
|
192
|
+
y=annotation_update_input.y,
|
|
193
|
+
width=annotation_update_input.width,
|
|
194
|
+
height=annotation_update_input.height,
|
|
195
|
+
)
|
|
196
|
+
for annotation_update_input in annotation_update_inputs
|
|
197
|
+
],
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@annotations_router.get("/annotations/{annotation_id}", response_model=AnnotationDetailsView)
|
|
202
|
+
def get_annotation(
|
|
203
|
+
session: SessionDep,
|
|
204
|
+
dataset_id: Annotated[ # noqa: ARG001
|
|
205
|
+
UUID,
|
|
206
|
+
Path(title="Dataset Id", description="The ID of the dataset"),
|
|
207
|
+
], # We need dataset_id because otherwise the path would not match
|
|
208
|
+
annotation_id: Annotated[UUID, Path(title="Annotation ID")],
|
|
209
|
+
) -> AnnotationBaseTable:
|
|
210
|
+
"""Retrieve an existing annotation from the database."""
|
|
211
|
+
return annotations_service.get_annotation_by_id(session=session, annotation_id=annotation_id)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@annotations_router.delete("/annotations/{annotation_id}/tag/{tag_id}")
|
|
215
|
+
def remove_tag_from_annotation(
|
|
216
|
+
session: SessionDep,
|
|
217
|
+
tag_id: UUID,
|
|
218
|
+
annotation_id: UUID,
|
|
219
|
+
) -> bool:
|
|
220
|
+
"""Remove annotation from a tag."""
|
|
221
|
+
annotation = annotation_resolver.get_by_id(session=session, annotation_id=annotation_id)
|
|
222
|
+
if not annotation:
|
|
223
|
+
raise HTTPException(
|
|
224
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
225
|
+
detail=f"Annotation {annotation_id} not found",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if not tag_resolver.remove_tag_from_annotation(
|
|
229
|
+
session=session, tag_id=tag_id, annotation=annotation
|
|
230
|
+
):
|
|
231
|
+
raise HTTPException(status_code=HTTP_STATUS_NOT_FOUND, detail=f"Tag {tag_id} not found")
|
|
232
|
+
|
|
233
|
+
return True
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""This module contains the API routes for managing annotation labels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
8
|
+
from sqlmodel import Session
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
|
+
|
|
11
|
+
from lightly_studio.api.db import get_session
|
|
12
|
+
from lightly_studio.api.routes.api.status import (
|
|
13
|
+
HTTP_STATUS_CREATED,
|
|
14
|
+
HTTP_STATUS_NOT_FOUND,
|
|
15
|
+
)
|
|
16
|
+
from lightly_studio.models.annotation_label import (
|
|
17
|
+
AnnotationLabelCreate,
|
|
18
|
+
AnnotationLabelTable,
|
|
19
|
+
)
|
|
20
|
+
from lightly_studio.resolvers import annotation_label_resolver
|
|
21
|
+
|
|
22
|
+
annotations_label_router = APIRouter()
|
|
23
|
+
SessionDep = Annotated[Session, Depends(get_session)]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@annotations_label_router.post(
|
|
27
|
+
"/annotation_labels",
|
|
28
|
+
status_code=HTTP_STATUS_CREATED,
|
|
29
|
+
)
|
|
30
|
+
def create_annotation_label(
|
|
31
|
+
input_label: AnnotationLabelCreate,
|
|
32
|
+
session: SessionDep,
|
|
33
|
+
) -> AnnotationLabelTable:
|
|
34
|
+
"""Create a new annotation label in the database."""
|
|
35
|
+
return annotation_label_resolver.create(session=session, label=input_label)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@annotations_label_router.get("/annotation_labels")
|
|
39
|
+
def read_annotation_labels(
|
|
40
|
+
session: SessionDep,
|
|
41
|
+
) -> list[AnnotationLabelTable]:
|
|
42
|
+
"""Retrieve a list of annotation labels from the database."""
|
|
43
|
+
return annotation_label_resolver.get_all(session=session)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@annotations_label_router.get("/annotation_labels/{label_id}")
|
|
47
|
+
def read_annotation_label(
|
|
48
|
+
label_id: UUID,
|
|
49
|
+
session: SessionDep,
|
|
50
|
+
) -> AnnotationLabelTable:
|
|
51
|
+
"""Retrieve a single annotation label from the database."""
|
|
52
|
+
label = annotation_label_resolver.get_by_id(session=session, label_id=label_id)
|
|
53
|
+
if not label:
|
|
54
|
+
raise HTTPException(
|
|
55
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
56
|
+
detail="Annotation label not found",
|
|
57
|
+
)
|
|
58
|
+
return label
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@annotations_label_router.put("/annotation_labels/{label_id}")
|
|
62
|
+
def update_annotation_label(
|
|
63
|
+
label_id: UUID,
|
|
64
|
+
label_input: AnnotationLabelCreate,
|
|
65
|
+
session: SessionDep,
|
|
66
|
+
) -> AnnotationLabelTable:
|
|
67
|
+
"""Update an existing annotation label in the database."""
|
|
68
|
+
label = annotation_label_resolver.update(
|
|
69
|
+
session=session, label_id=label_id, label_data=label_input
|
|
70
|
+
)
|
|
71
|
+
if not label:
|
|
72
|
+
raise HTTPException(
|
|
73
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
74
|
+
detail="Annotation label not found",
|
|
75
|
+
)
|
|
76
|
+
return label
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@annotations_label_router.delete("/annotation_labels/{label_id}")
|
|
80
|
+
def delete_annotation_label(
|
|
81
|
+
label_id: UUID,
|
|
82
|
+
session: SessionDep,
|
|
83
|
+
) -> dict[str, str]:
|
|
84
|
+
"""Delete an annotation label from the database."""
|
|
85
|
+
if not annotation_label_resolver.delete(session=session, label_id=label_id):
|
|
86
|
+
raise HTTPException(
|
|
87
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
88
|
+
detail="Annotation label not found",
|
|
89
|
+
)
|
|
90
|
+
return {"status": "deleted"}
|