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.

Files changed (219) hide show
  1. lightly_studio/__init__.py +11 -0
  2. lightly_studio/api/__init__.py +0 -0
  3. lightly_studio/api/app.py +110 -0
  4. lightly_studio/api/cache.py +77 -0
  5. lightly_studio/api/db.py +133 -0
  6. lightly_studio/api/db_tables.py +32 -0
  7. lightly_studio/api/features.py +7 -0
  8. lightly_studio/api/routes/api/annotation.py +233 -0
  9. lightly_studio/api/routes/api/annotation_label.py +90 -0
  10. lightly_studio/api/routes/api/annotation_task.py +38 -0
  11. lightly_studio/api/routes/api/classifier.py +387 -0
  12. lightly_studio/api/routes/api/dataset.py +182 -0
  13. lightly_studio/api/routes/api/dataset_tag.py +257 -0
  14. lightly_studio/api/routes/api/exceptions.py +96 -0
  15. lightly_studio/api/routes/api/features.py +17 -0
  16. lightly_studio/api/routes/api/metadata.py +37 -0
  17. lightly_studio/api/routes/api/metrics.py +80 -0
  18. lightly_studio/api/routes/api/sample.py +196 -0
  19. lightly_studio/api/routes/api/settings.py +45 -0
  20. lightly_studio/api/routes/api/status.py +19 -0
  21. lightly_studio/api/routes/api/text_embedding.py +48 -0
  22. lightly_studio/api/routes/api/validators.py +17 -0
  23. lightly_studio/api/routes/healthz.py +13 -0
  24. lightly_studio/api/routes/images.py +104 -0
  25. lightly_studio/api/routes/webapp.py +51 -0
  26. lightly_studio/api/server.py +82 -0
  27. lightly_studio/core/__init__.py +0 -0
  28. lightly_studio/core/dataset.py +523 -0
  29. lightly_studio/core/sample.py +77 -0
  30. lightly_studio/core/start_gui.py +15 -0
  31. lightly_studio/dataset/__init__.py +0 -0
  32. lightly_studio/dataset/edge_embedding_generator.py +144 -0
  33. lightly_studio/dataset/embedding_generator.py +91 -0
  34. lightly_studio/dataset/embedding_manager.py +163 -0
  35. lightly_studio/dataset/env.py +16 -0
  36. lightly_studio/dataset/file_utils.py +35 -0
  37. lightly_studio/dataset/loader.py +622 -0
  38. lightly_studio/dataset/mobileclip_embedding_generator.py +144 -0
  39. lightly_studio/dist_lightly_studio_view_app/_app/env.js +1 -0
  40. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/0.DenzbfeK.css +1 -0
  41. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/LightlyLogo.BNjCIww-.png +0 -0
  42. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans- +0 -0
  43. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Bold.DGvYQtcs.ttf +0 -0
  44. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Italic-VariableFont_wdth_wght.B4AZ-wl6.ttf +0 -0
  45. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-Regular.DxJTClRG.ttf +0 -0
  46. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-SemiBold.D3TTYgdB.ttf +0 -0
  47. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/OpenSans-VariableFont_wdth_wght.BZBpG5Iz.ttf +0 -0
  48. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.OwPEPQZu.css +1 -0
  49. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/SelectableSvgGroup.b653GmVf.css +1 -0
  50. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/_layout.T-zjSUd3.css +1 -0
  51. lightly_studio/dist_lightly_studio_view_app/_app/immutable/assets/useFeatureFlags.CV-KWLNP.css +1 -0
  52. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/69_IOA4Y.js +1 -0
  53. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B2FVR0s0.js +1 -0
  54. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B90CZVMX.js +1 -0
  55. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/B9zumHo5.js +1 -0
  56. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BJXwVxaE.js +1 -0
  57. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bsi3UGy5.js +1 -0
  58. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bu7uvVrG.js +1 -0
  59. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/Bx1xMsFy.js +1 -0
  60. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/BylOuP6i.js +1 -0
  61. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/C8I8rFJQ.js +1 -0
  62. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CDnpyLsT.js +1 -0
  63. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CWj6FrbW.js +1 -0
  64. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CYgJF_JY.js +1 -0
  65. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CcaPhhk3.js +1 -0
  66. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CvOmgdoc.js +93 -0
  67. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/CxtLVaYz.js +3 -0
  68. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D5-A_Ffd.js +4 -0
  69. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6RI2Zrd.js +1 -0
  70. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D6su9Aln.js +1 -0
  71. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/D98V7j6A.js +1 -0
  72. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIRAtgl0.js +1 -0
  73. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DIeogL5L.js +1 -0
  74. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DOlTMNyt.js +1 -0
  75. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjUWrjOv.js +1 -0
  76. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/DjfY96ND.js +1 -0
  77. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/H7C68rOM.js +1 -0
  78. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/O-EABkf9.js +1 -0
  79. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/XO7A28GO.js +1 -0
  80. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/hQVEETDE.js +1 -0
  81. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/l7KrR96u.js +1 -0
  82. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/nAHhluT7.js +1 -0
  83. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/r64xT6ao.js +1 -0
  84. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/vC4nQVEB.js +1 -0
  85. lightly_studio/dist_lightly_studio_view_app/_app/immutable/chunks/x9G_hzyY.js +1 -0
  86. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/app.CjnvpsmS.js +2 -0
  87. lightly_studio/dist_lightly_studio_view_app/_app/immutable/entry/start.0o1H7wM9.js +1 -0
  88. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/0.XRq_TUwu.js +1 -0
  89. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/1.B4rNYwVp.js +1 -0
  90. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/10.DfBwOEhN.js +1 -0
  91. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/11.CWG1ehzT.js +1 -0
  92. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/12.CwF2_8mP.js +1 -0
  93. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/2.CS4muRY-.js +6 -0
  94. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/3.CWHpKonm.js +1 -0
  95. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/4.OUWOLQeV.js +1 -0
  96. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/5.Dm6t9F5W.js +1 -0
  97. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/6.Bw5ck4gK.js +1 -0
  98. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/7.CF0EDTR6.js +1 -0
  99. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/8.Cw30LEcV.js +1 -0
  100. lightly_studio/dist_lightly_studio_view_app/_app/immutable/nodes/9.CPu3CiBc.js +1 -0
  101. lightly_studio/dist_lightly_studio_view_app/_app/version.json +1 -0
  102. lightly_studio/dist_lightly_studio_view_app/apple-touch-icon-precomposed.png +0 -0
  103. lightly_studio/dist_lightly_studio_view_app/apple-touch-icon.png +0 -0
  104. lightly_studio/dist_lightly_studio_view_app/favicon.png +0 -0
  105. lightly_studio/dist_lightly_studio_view_app/index.html +44 -0
  106. lightly_studio/examples/example.py +23 -0
  107. lightly_studio/examples/example_metadata.py +338 -0
  108. lightly_studio/examples/example_selection.py +39 -0
  109. lightly_studio/examples/example_split_work.py +67 -0
  110. lightly_studio/examples/example_v2.py +21 -0
  111. lightly_studio/export_schema.py +18 -0
  112. lightly_studio/few_shot_classifier/__init__.py +0 -0
  113. lightly_studio/few_shot_classifier/classifier.py +80 -0
  114. lightly_studio/few_shot_classifier/classifier_manager.py +663 -0
  115. lightly_studio/few_shot_classifier/random_forest_classifier.py +489 -0
  116. lightly_studio/metadata/complex_metadata.py +47 -0
  117. lightly_studio/metadata/gps_coordinate.py +41 -0
  118. lightly_studio/metadata/metadata_protocol.py +17 -0
  119. lightly_studio/metrics/__init__.py +0 -0
  120. lightly_studio/metrics/detection/__init__.py +0 -0
  121. lightly_studio/metrics/detection/map.py +268 -0
  122. lightly_studio/models/__init__.py +1 -0
  123. lightly_studio/models/annotation/__init__.py +0 -0
  124. lightly_studio/models/annotation/annotation_base.py +171 -0
  125. lightly_studio/models/annotation/instance_segmentation.py +56 -0
  126. lightly_studio/models/annotation/links.py +17 -0
  127. lightly_studio/models/annotation/object_detection.py +47 -0
  128. lightly_studio/models/annotation/semantic_segmentation.py +44 -0
  129. lightly_studio/models/annotation_label.py +47 -0
  130. lightly_studio/models/annotation_task.py +28 -0
  131. lightly_studio/models/classifier.py +20 -0
  132. lightly_studio/models/dataset.py +84 -0
  133. lightly_studio/models/embedding_model.py +30 -0
  134. lightly_studio/models/metadata.py +208 -0
  135. lightly_studio/models/sample.py +180 -0
  136. lightly_studio/models/sample_embedding.py +37 -0
  137. lightly_studio/models/settings.py +60 -0
  138. lightly_studio/models/tag.py +96 -0
  139. lightly_studio/py.typed +0 -0
  140. lightly_studio/resolvers/__init__.py +7 -0
  141. lightly_studio/resolvers/annotation_label_resolver/__init__.py +21 -0
  142. lightly_studio/resolvers/annotation_label_resolver/create.py +27 -0
  143. lightly_studio/resolvers/annotation_label_resolver/delete.py +28 -0
  144. lightly_studio/resolvers/annotation_label_resolver/get_all.py +22 -0
  145. lightly_studio/resolvers/annotation_label_resolver/get_by_id.py +24 -0
  146. lightly_studio/resolvers/annotation_label_resolver/get_by_ids.py +25 -0
  147. lightly_studio/resolvers/annotation_label_resolver/get_by_label_name.py +24 -0
  148. lightly_studio/resolvers/annotation_label_resolver/names_by_ids.py +25 -0
  149. lightly_studio/resolvers/annotation_label_resolver/update.py +38 -0
  150. lightly_studio/resolvers/annotation_resolver/__init__.py +33 -0
  151. lightly_studio/resolvers/annotation_resolver/count_annotations_by_dataset.py +120 -0
  152. lightly_studio/resolvers/annotation_resolver/create.py +19 -0
  153. lightly_studio/resolvers/annotation_resolver/create_many.py +96 -0
  154. lightly_studio/resolvers/annotation_resolver/delete_annotation.py +45 -0
  155. lightly_studio/resolvers/annotation_resolver/delete_annotations.py +56 -0
  156. lightly_studio/resolvers/annotation_resolver/get_all.py +74 -0
  157. lightly_studio/resolvers/annotation_resolver/get_by_id.py +18 -0
  158. lightly_studio/resolvers/annotation_resolver/update_annotation_label.py +144 -0
  159. lightly_studio/resolvers/annotation_resolver/update_bounding_box.py +68 -0
  160. lightly_studio/resolvers/annotation_task_resolver.py +31 -0
  161. lightly_studio/resolvers/annotations/__init__.py +1 -0
  162. lightly_studio/resolvers/annotations/annotations_filter.py +89 -0
  163. lightly_studio/resolvers/dataset_resolver.py +278 -0
  164. lightly_studio/resolvers/embedding_model_resolver.py +100 -0
  165. lightly_studio/resolvers/metadata_resolver/__init__.py +15 -0
  166. lightly_studio/resolvers/metadata_resolver/metadata_filter.py +163 -0
  167. lightly_studio/resolvers/metadata_resolver/sample/__init__.py +21 -0
  168. lightly_studio/resolvers/metadata_resolver/sample/bulk_set_metadata.py +48 -0
  169. lightly_studio/resolvers/metadata_resolver/sample/get_by_sample_id.py +24 -0
  170. lightly_studio/resolvers/metadata_resolver/sample/get_metadata_info.py +104 -0
  171. lightly_studio/resolvers/metadata_resolver/sample/get_value_for_sample.py +27 -0
  172. lightly_studio/resolvers/metadata_resolver/sample/set_value_for_sample.py +53 -0
  173. lightly_studio/resolvers/sample_embedding_resolver.py +86 -0
  174. lightly_studio/resolvers/sample_resolver.py +249 -0
  175. lightly_studio/resolvers/samples_filter.py +81 -0
  176. lightly_studio/resolvers/settings_resolver.py +58 -0
  177. lightly_studio/resolvers/tag_resolver.py +276 -0
  178. lightly_studio/selection/README.md +6 -0
  179. lightly_studio/selection/mundig.py +105 -0
  180. lightly_studio/selection/select.py +96 -0
  181. lightly_studio/selection/select_via_db.py +93 -0
  182. lightly_studio/selection/selection_config.py +31 -0
  183. lightly_studio/services/annotations_service/__init__.py +21 -0
  184. lightly_studio/services/annotations_service/get_annotation_by_id.py +31 -0
  185. lightly_studio/services/annotations_service/update_annotation.py +65 -0
  186. lightly_studio/services/annotations_service/update_annotation_label.py +48 -0
  187. lightly_studio/services/annotations_service/update_annotations.py +29 -0
  188. lightly_studio/setup_logging.py +19 -0
  189. lightly_studio/type_definitions.py +19 -0
  190. lightly_studio/vendor/ACKNOWLEDGEMENTS +422 -0
  191. lightly_studio/vendor/LICENSE +31 -0
  192. lightly_studio/vendor/LICENSE_weights_data +50 -0
  193. lightly_studio/vendor/README.md +5 -0
  194. lightly_studio/vendor/__init__.py +1 -0
  195. lightly_studio/vendor/mobileclip/__init__.py +96 -0
  196. lightly_studio/vendor/mobileclip/clip.py +77 -0
  197. lightly_studio/vendor/mobileclip/configs/mobileclip_b.json +18 -0
  198. lightly_studio/vendor/mobileclip/configs/mobileclip_s0.json +18 -0
  199. lightly_studio/vendor/mobileclip/configs/mobileclip_s1.json +18 -0
  200. lightly_studio/vendor/mobileclip/configs/mobileclip_s2.json +18 -0
  201. lightly_studio/vendor/mobileclip/image_encoder.py +67 -0
  202. lightly_studio/vendor/mobileclip/logger.py +154 -0
  203. lightly_studio/vendor/mobileclip/models/__init__.py +10 -0
  204. lightly_studio/vendor/mobileclip/models/mci.py +933 -0
  205. lightly_studio/vendor/mobileclip/models/vit.py +433 -0
  206. lightly_studio/vendor/mobileclip/modules/__init__.py +4 -0
  207. lightly_studio/vendor/mobileclip/modules/common/__init__.py +4 -0
  208. lightly_studio/vendor/mobileclip/modules/common/mobileone.py +341 -0
  209. lightly_studio/vendor/mobileclip/modules/common/transformer.py +451 -0
  210. lightly_studio/vendor/mobileclip/modules/image/__init__.py +4 -0
  211. lightly_studio/vendor/mobileclip/modules/image/image_projection.py +113 -0
  212. lightly_studio/vendor/mobileclip/modules/image/replknet.py +188 -0
  213. lightly_studio/vendor/mobileclip/modules/text/__init__.py +4 -0
  214. lightly_studio/vendor/mobileclip/modules/text/repmixer.py +281 -0
  215. lightly_studio/vendor/mobileclip/modules/text/tokenizer.py +38 -0
  216. lightly_studio/vendor/mobileclip/text_encoder.py +245 -0
  217. lightly_studio-0.3.1.dist-info/METADATA +520 -0
  218. lightly_studio-0.3.1.dist-info/RECORD +219 -0
  219. 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
@@ -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,7 @@
1
+ """Global list fo the active features."""
2
+
3
+ # TODO(Kondrat 04/25): Pass the feature flag to app
4
+ # https://linear.app/lightly/issue/LIG-6708/introduce-apifeatures-endpoint
5
+ from typing import List
6
+
7
+ lightly_studio_active_features: List[str] = []
@@ -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"}