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,45 @@
|
|
|
1
|
+
"""This module contains the API routes for user settings."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from sqlmodel import Session
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
|
|
7
|
+
from lightly_studio.api.db import get_session
|
|
8
|
+
from lightly_studio.models.settings import SettingView
|
|
9
|
+
from lightly_studio.resolvers import settings_resolver
|
|
10
|
+
|
|
11
|
+
settings_router = APIRouter(tags=["settings"])
|
|
12
|
+
|
|
13
|
+
SessionDep = Annotated[Session, Depends(get_session)]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@settings_router.get("/settings")
|
|
17
|
+
def get_settings(
|
|
18
|
+
session: SessionDep,
|
|
19
|
+
) -> SettingView:
|
|
20
|
+
"""Get the current settings.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
session: Database session.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The current settings.
|
|
27
|
+
"""
|
|
28
|
+
return settings_resolver.get_settings(session=session)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@settings_router.post("/settings")
|
|
32
|
+
def set_settings(
|
|
33
|
+
settings: SettingView,
|
|
34
|
+
session: SessionDep,
|
|
35
|
+
) -> SettingView:
|
|
36
|
+
"""Update user settings.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
settings: New settings to apply.
|
|
40
|
+
session: Database session.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Updated settings.
|
|
44
|
+
"""
|
|
45
|
+
return settings_resolver.set_settings(session=session, settings=settings)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""HTTP status codes for use in tests and responses."""
|
|
2
|
+
|
|
3
|
+
HTTP_STATUS_OK = 200
|
|
4
|
+
HTTP_STATUS_CREATED = 201
|
|
5
|
+
HTTP_STATUS_ACCEPTED = 202
|
|
6
|
+
HTTP_STATUS_NO_CONTENT = 204
|
|
7
|
+
|
|
8
|
+
HTTP_STATUS_BAD_REQUEST = 400
|
|
9
|
+
HTTP_STATUS_UNAUTHORIZED = 401
|
|
10
|
+
HTTP_STATUS_FORBIDDEN = 403
|
|
11
|
+
HTTP_STATUS_NOT_FOUND = 404
|
|
12
|
+
HTTP_STATUS_CONFLICT = 409
|
|
13
|
+
HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE = 415
|
|
14
|
+
HTTP_STATUS_UNPRECESSABLE_ENTITY = 422
|
|
15
|
+
|
|
16
|
+
HTTP_STATUS_INTERNAL_SERVER_ERROR = 500
|
|
17
|
+
HTTP_STATUS_NOT_IMPLEMENTED = 501
|
|
18
|
+
HTTP_STATUS_BAD_GATEWAY = 502
|
|
19
|
+
HTTP_STATUS_SERVICE_UNAVAILABLE = 503
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""This module contains the API routes for managing text embedding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
|
+
|
|
11
|
+
from lightly_studio.api.routes.api.status import (
|
|
12
|
+
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
|
13
|
+
)
|
|
14
|
+
from lightly_studio.dataset.embedding_manager import (
|
|
15
|
+
EmbeddingManager,
|
|
16
|
+
EmbeddingManagerProvider,
|
|
17
|
+
TextEmbedQuery,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
text_embedding_router = APIRouter()
|
|
21
|
+
# Define a type alias for the EmbeddingManager dependency
|
|
22
|
+
EmbeddingManagerDep = Annotated[
|
|
23
|
+
EmbeddingManager,
|
|
24
|
+
Depends(lambda: EmbeddingManagerProvider.get_embedding_manager()),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@text_embedding_router.get("/text_embedding/embed_text", response_model=List[float])
|
|
29
|
+
def embed_text(
|
|
30
|
+
embedding_manager: EmbeddingManagerDep,
|
|
31
|
+
query_text: str = Query(..., description="The text to embed."),
|
|
32
|
+
embedding_model_id: Annotated[
|
|
33
|
+
UUID | None,
|
|
34
|
+
Query(..., description="The ID of the embedding model to use."),
|
|
35
|
+
] = None,
|
|
36
|
+
) -> list[float]:
|
|
37
|
+
"""Retrieve embeddings for the input text."""
|
|
38
|
+
try:
|
|
39
|
+
text_embeddings = embedding_manager.embed_text(
|
|
40
|
+
TextEmbedQuery(query_text, embedding_model_id)
|
|
41
|
+
)
|
|
42
|
+
except ValueError as exc:
|
|
43
|
+
raise HTTPException(
|
|
44
|
+
status_code=HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
|
45
|
+
detail=f"{exc}",
|
|
46
|
+
) from None
|
|
47
|
+
|
|
48
|
+
return text_embeddings
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""General LightlyStudio API models."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Paginated(BaseModel):
|
|
7
|
+
"""Paginated query parameters."""
|
|
8
|
+
|
|
9
|
+
offset: int = Field(0, ge=0, description="Offset for pagination")
|
|
10
|
+
limit: int = Field(100, gt=0, le=100, description="Limit for pagination")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PaginatedWithCursor(BaseModel):
|
|
14
|
+
"""Paginated query parameters."""
|
|
15
|
+
|
|
16
|
+
offset: int = Field(0, ge=0, description="Offset for pagination", alias="cursor")
|
|
17
|
+
limit: int = Field(100, gt=0, le=100, description="Limit for pagination")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""This module contains the API routes for managing datasets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
|
|
7
|
+
health_router = APIRouter()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@health_router.get("/healthz", include_in_schema=False)
|
|
11
|
+
def health_check() -> dict[str, str]:
|
|
12
|
+
"""Health check endpoint to verify the service is running."""
|
|
13
|
+
return {"status": "healthy"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Image serving endpoint that supports local files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Generator
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
9
|
+
from fastapi.responses import StreamingResponse
|
|
10
|
+
from sqlmodel import Session
|
|
11
|
+
|
|
12
|
+
from lightly_studio.api import db
|
|
13
|
+
from lightly_studio.api.routes.api import status
|
|
14
|
+
from lightly_studio.models import sample
|
|
15
|
+
|
|
16
|
+
app_router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app_router.get("/sample/{sample_id}")
|
|
20
|
+
async def serve_image_by_sample_id(
|
|
21
|
+
sample_id: str,
|
|
22
|
+
session: Session = Depends(db.get_session), # noqa: B008
|
|
23
|
+
) -> StreamingResponse:
|
|
24
|
+
"""Serve an image by sample ID.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
sample_id: The ID of the sample.
|
|
28
|
+
session: Database session.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
StreamingResponse with the image data.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
HTTPException: If the sample is not found or the file is not accessible.
|
|
35
|
+
"""
|
|
36
|
+
# Retrieve the sample from the database.
|
|
37
|
+
sample_record = session.get(sample.SampleTable, sample_id)
|
|
38
|
+
if not sample_record:
|
|
39
|
+
raise HTTPException(
|
|
40
|
+
status_code=status.HTTP_STATUS_NOT_FOUND,
|
|
41
|
+
detail=f"Sample not found: {sample_id}",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
file_path = sample_record.file_path_abs
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# Open the file.
|
|
48
|
+
with open(file_path, "rb") as file:
|
|
49
|
+
content = file.read()
|
|
50
|
+
|
|
51
|
+
# Determine content type based on file extension.
|
|
52
|
+
content_type = _get_content_type(file_path)
|
|
53
|
+
|
|
54
|
+
# Create a streaming response.
|
|
55
|
+
def generate() -> Generator[bytes, None, None]:
|
|
56
|
+
yield content
|
|
57
|
+
|
|
58
|
+
return StreamingResponse(
|
|
59
|
+
generate(),
|
|
60
|
+
media_type=content_type,
|
|
61
|
+
headers={
|
|
62
|
+
# Cache for 1 hour
|
|
63
|
+
"Cache-Control": "public, max-age=3600",
|
|
64
|
+
"Content-Length": str(len(content)),
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
except FileNotFoundError as exc:
|
|
69
|
+
raise HTTPException(
|
|
70
|
+
status_code=status.HTTP_STATUS_NOT_FOUND,
|
|
71
|
+
detail=f"File not found: {file_path}",
|
|
72
|
+
) from exc
|
|
73
|
+
except OSError as exc:
|
|
74
|
+
raise HTTPException(
|
|
75
|
+
status_code=status.HTTP_STATUS_NOT_FOUND,
|
|
76
|
+
detail=f"Error accessing file {file_path}: {exc.strerror}",
|
|
77
|
+
) from exc
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_content_type(file_path: str) -> str:
|
|
81
|
+
"""Get the appropriate content type for a file based on its extension.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
file_path: Path to the file.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
MIME type string.
|
|
88
|
+
"""
|
|
89
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
90
|
+
|
|
91
|
+
content_types = {
|
|
92
|
+
".png": "image/png",
|
|
93
|
+
".jpg": "image/jpeg",
|
|
94
|
+
".jpeg": "image/jpeg",
|
|
95
|
+
".gif": "image/gif",
|
|
96
|
+
".webp": "image/webp",
|
|
97
|
+
".bmp": "image/bmp",
|
|
98
|
+
".tiff": "image/tiff",
|
|
99
|
+
".mov": "video/quicktime",
|
|
100
|
+
".mp4": "video/mp4",
|
|
101
|
+
".avi": "video/x-msvideo",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return content_types.get(ext, "application/octet-stream")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""This module contains the API routes for managing datasets."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
from fastapi.responses import FileResponse
|
|
7
|
+
|
|
8
|
+
from .api.status import HTTP_STATUS_NOT_FOUND
|
|
9
|
+
|
|
10
|
+
app_router = APIRouter()
|
|
11
|
+
|
|
12
|
+
# Get the current project root directory
|
|
13
|
+
project_root = Path(__file__).resolve().parent.parent.parent
|
|
14
|
+
|
|
15
|
+
webapp_dir = project_root / "dist_lightly_studio_view_app"
|
|
16
|
+
|
|
17
|
+
# Check if the webapp directory exists and raise an error if it doesn't
|
|
18
|
+
if not webapp_dir.exists():
|
|
19
|
+
raise RuntimeError(f"Directory '{webapp_dir}' does not exist in '{project_root}'")
|
|
20
|
+
|
|
21
|
+
# Ensure the path is absolute
|
|
22
|
+
webapp_dir = webapp_dir.resolve()
|
|
23
|
+
|
|
24
|
+
# ensure the webapp index.html file exists
|
|
25
|
+
index_file = webapp_dir / "index.html"
|
|
26
|
+
if not index_file.exists():
|
|
27
|
+
raise RuntimeError("No index file. Did you forget to build the webapp?")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app_router.get("/{path:path}", include_in_schema=False)
|
|
31
|
+
async def serve_static_webapp_files_or_default_index_file(
|
|
32
|
+
path: str,
|
|
33
|
+
) -> FileResponse:
|
|
34
|
+
"""Serve static files of webapp or serve the index.html file.
|
|
35
|
+
|
|
36
|
+
Try to serve static files with file extensions or return 404.
|
|
37
|
+
If no file extension, return the main webapp index.html.
|
|
38
|
+
"""
|
|
39
|
+
file_path = webapp_dir / path
|
|
40
|
+
|
|
41
|
+
# if file has an extension, try to return the file
|
|
42
|
+
if file_path.suffix:
|
|
43
|
+
if not file_path.exists() or not file_path.is_file():
|
|
44
|
+
raise HTTPException(
|
|
45
|
+
status_code=HTTP_STATUS_NOT_FOUND,
|
|
46
|
+
detail=f"File '{path}' not found",
|
|
47
|
+
)
|
|
48
|
+
return FileResponse(file_path)
|
|
49
|
+
|
|
50
|
+
# if file has no extension, return the index.html file regardless of path
|
|
51
|
+
return FileResponse(index_file)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""This module contains the Server class for running the API using Uvicorn."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
import uvicorn
|
|
7
|
+
|
|
8
|
+
from lightly_studio.api.app import app
|
|
9
|
+
from lightly_studio.dataset import env
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Server:
|
|
13
|
+
"""This class represents a server for running the API using Uvicorn."""
|
|
14
|
+
|
|
15
|
+
port: int
|
|
16
|
+
host: str
|
|
17
|
+
|
|
18
|
+
def __init__(self, host: str, port: int) -> None:
|
|
19
|
+
"""Initialize the Server with host and port.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
host (str): The hostname to bind the server to.
|
|
23
|
+
port (int): The port number to run the server on.
|
|
24
|
+
"""
|
|
25
|
+
self.host = host
|
|
26
|
+
self.port = _get_available_port(host=host, preferred_port=port)
|
|
27
|
+
if port != self.port:
|
|
28
|
+
env.LIGHTLY_STUDIO_PORT = self.port
|
|
29
|
+
env.APP_URL = f"{env.LIGHTLY_STUDIO_PROTOCOL}://{env.LIGHTLY_STUDIO_HOST}:{env.LIGHTLY_STUDIO_PORT}"
|
|
30
|
+
|
|
31
|
+
def start(self) -> None:
|
|
32
|
+
"""Start the API server using Uvicorn."""
|
|
33
|
+
# start the app
|
|
34
|
+
uvicorn.run(app, host=self.host, port=self.port, http="h11")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_available_port(host: str, preferred_port: int, max_tries: int = 50) -> int:
|
|
38
|
+
"""Get an available port, if possible, otherwise a random one.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
host: The hostname or IP address to bind to.
|
|
42
|
+
preferred_port: The port to try first.
|
|
43
|
+
max_tries: Maximum number of random ports to try.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
RuntimeError if it cannot find an available port.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
An available port number.
|
|
50
|
+
"""
|
|
51
|
+
if _is_port_available(host=host, port=preferred_port):
|
|
52
|
+
return preferred_port
|
|
53
|
+
|
|
54
|
+
# Try random ports in the range 1024-65535
|
|
55
|
+
for _ in range(max_tries):
|
|
56
|
+
port = random.randint(1024, 65535)
|
|
57
|
+
if _is_port_available(host=host, port=port):
|
|
58
|
+
return port
|
|
59
|
+
|
|
60
|
+
raise RuntimeError("Could not find an available port.")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_port_available(host: str, port: int) -> bool:
|
|
64
|
+
# Determine address family based on host.
|
|
65
|
+
try:
|
|
66
|
+
socket.inet_pton(socket.AF_INET, host)
|
|
67
|
+
families = [socket.AF_INET]
|
|
68
|
+
except OSError:
|
|
69
|
+
try:
|
|
70
|
+
socket.inet_pton(socket.AF_INET6, host)
|
|
71
|
+
families = [socket.AF_INET6]
|
|
72
|
+
except OSError:
|
|
73
|
+
# Fallback for hostnames like 'localhost'
|
|
74
|
+
families = [socket.AF_INET, socket.AF_INET6]
|
|
75
|
+
|
|
76
|
+
for family in families:
|
|
77
|
+
with socket.socket(family, socket.SOCK_STREAM) as s:
|
|
78
|
+
try:
|
|
79
|
+
s.bind((host, port))
|
|
80
|
+
except OSError:
|
|
81
|
+
return False
|
|
82
|
+
return True
|
|
File without changes
|