datamint 1.6.3.post1__py3-none-any.whl → 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of datamint might be problematic. Click here for more details.

@@ -0,0 +1,221 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field, asdict
3
+ from typing import Optional, Any, TYPE_CHECKING
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ import logging
7
+ import numpy as np
8
+ from PIL import Image
9
+ import json
10
+
11
+ # if TYPE_CHECKING:
12
+ # from datamint.apihandler.annotation_api_handler import AnnotationAPIHandler
13
+
14
+ _LOGGER = logging.getLogger(__name__)
15
+
16
+
17
+ # Map API field names to class attributes
18
+ _FIELD_MAPPING = {
19
+ 'type':'annotation_type',
20
+ 'name': 'identifier',
21
+ 'added_by': 'created_by',
22
+ 'index': 'frame_index',
23
+ }
24
+
25
+ @dataclass
26
+ class Annotation:
27
+ """
28
+ Class representing an annotation from the Datamint API.
29
+
30
+ This class stores annotation data and provides methods for loading
31
+ and saving annotations through the API handler.
32
+
33
+ Args:
34
+ id: Unique identifier for the annotation
35
+ identifier: The annotation identifier/label name
36
+ scope: Whether annotation applies to 'frame' or 'image'
37
+ annotation_type: Type of annotation ('segmentation', 'label', 'category', etc.)
38
+ resource_id: ID of the resource this annotation belongs to
39
+ annotation_worklist_id: ID of the annotation worklist
40
+ created_by: Email of the user who created the annotation
41
+ status: Status of the annotation ('published', 'new', etc.)
42
+ frame_index: Frame index for frame-scoped annotations
43
+ text_value: Text value for category annotations
44
+ numeric_value: Numeric value for numeric annotations
45
+ units: Units for numeric annotations
46
+ geometry: Geometry data for geometric annotations
47
+ created_at: When the annotation was created
48
+ approved_at: When the annotation was approved
49
+ approved_by: Who approved the annotation
50
+ associated_file: Path to associated file (for segmentations)
51
+ deleted: Whether the annotation is deleted
52
+ deleted_at: When the annotation was deleted
53
+ deleted_by: Who deleted the annotation
54
+ created_by_model: Model ID if created by AI
55
+ old_geometry: Previous geometry data
56
+ set_name: Set name for grouped annotations
57
+ resource_filename: Filename of the associated resource
58
+ resource_modality: Modality of the associated resource
59
+ annotation_worklist_name: Name of the annotation worklist
60
+ user_info: Information about the user who created the annotation
61
+ values: Additional values
62
+ """
63
+
64
+ id: str
65
+ identifier: str
66
+ scope: str
67
+ annotation_type: str
68
+ resource_id: str
69
+ created_by: str
70
+ annotation_worklist_id: Optional[str] = None
71
+ status: Optional[str] = None
72
+ frame_index: Optional[int] = None
73
+ text_value: Optional[str] = None
74
+ numeric_value: Optional[float] = None
75
+ units: Optional[str] = None
76
+ geometry: list[Any] = field(default_factory=list)
77
+ created_at: Optional[str] = None
78
+ approved_at: Optional[str] = None
79
+ approved_by: Optional[str] = None
80
+ associated_file: Optional[str] = None
81
+ file: Optional[str] = None
82
+ deleted: bool = False
83
+ deleted_at: Optional[str] = None
84
+ deleted_by: Optional[str] = None
85
+ created_by_model: Optional[str] = None
86
+ old_geometry: Optional[Any] = None
87
+ set_name: Optional[str] = None
88
+ resource_filename: Optional[str] = None
89
+ resource_modality: Optional[str] = None
90
+ annotation_worklist_name: Optional[str] = None
91
+ user_info: Optional[dict[str, str]] = None
92
+ values: Optional[Any] = None
93
+
94
+ @classmethod
95
+ def from_dict(cls, data: dict[str, Any]) -> Annotation:
96
+ """
97
+ Create an Annotation instance from a dictionary.
98
+
99
+ Args:
100
+ data: Dictionary containing annotation data from API
101
+
102
+ Returns:
103
+ Annotation instance
104
+ """
105
+
106
+
107
+ # Convert field names and filter valid fields
108
+ converted_data = {}
109
+ for key, value in data.items():
110
+ # Map field names if needed
111
+ mapped_key = _FIELD_MAPPING.get(key, key)
112
+ converted_data[mapped_key] = value
113
+
114
+ if 'scope' not in converted_data:
115
+ converted_data['scope'] = 'image' if converted_data.get('frame_index') is None else 'frame'
116
+
117
+ if converted_data['annotation_type'] in ['segmentation']:
118
+ if converted_data.get('file') is None:
119
+ raise ValueError(f"Segmentation annotations must have an associated file. {data}")
120
+
121
+ # Create instance with only valid fields
122
+ valid_fields = {f.name for f in cls.__dataclass_fields__.values()}
123
+ filtered_data = {k: v for k, v in converted_data.items() if k in valid_fields}
124
+
125
+ return cls(**filtered_data)
126
+
127
+ def to_dict(self) -> dict[str, Any]:
128
+ """
129
+ Convert the annotation to a dictionary format.
130
+
131
+ Returns:
132
+ Dictionary representation of the annotation
133
+ """
134
+ result = {}
135
+ for key, value in self.__dict__.items():
136
+ # Handle special serialization cases
137
+ if isinstance(value, (np.ndarray, np.generic)):
138
+ value = value.tolist()
139
+ elif isinstance(value, datetime):
140
+ value = value.isoformat()
141
+ elif isinstance(value, Path):
142
+ value = str(value)
143
+
144
+ result[key] = value
145
+ if self.annotation_type == 'segmentation' and 'file' not in result:
146
+ raise ValueError(f"Segmentation annotations must have an associated file. {self}")
147
+ return result
148
+
149
+ @property
150
+ def name(self) -> str:
151
+ """Get the annotation name (alias for identifier)."""
152
+ return self.identifier
153
+
154
+ @property
155
+ def type(self) -> str:
156
+ """Get the annotation type."""
157
+ return self.annotation_type
158
+
159
+ @property
160
+ def value(self) -> Optional[str]:
161
+ """Get the annotation value (for category annotations)."""
162
+ return self.text_value
163
+
164
+ @property
165
+ def index(self) -> Optional[int]:
166
+ """Get the frame index (alias for frame_index)."""
167
+ return self.frame_index
168
+
169
+ @property
170
+ def added_by(self) -> str:
171
+ """Get the creator email (alias for created_by)."""
172
+ return self.created_by
173
+
174
+ # @property
175
+ # def file(self) -> Optional[str]:
176
+ # """Get the associated file path."""
177
+ # return self.associated_file
178
+
179
+ # @file.setter
180
+ # def file(self, value: Optional[str]) -> None:
181
+ # """Set the associated file path."""
182
+ # self.associated_file = value
183
+
184
+ def is_segmentation(self) -> bool:
185
+ """Check if this is a segmentation annotation."""
186
+ return self.annotation_type == 'segmentation'
187
+
188
+ def is_label(self) -> bool:
189
+ """Check if this is a label annotation."""
190
+ return self.annotation_type == 'label'
191
+
192
+ def is_category(self) -> bool:
193
+ """Check if this is a category annotation."""
194
+ return self.annotation_type == 'category'
195
+
196
+ def is_frame_scoped(self) -> bool:
197
+ """Check if this annotation is frame-scoped."""
198
+ return self.scope == 'frame'
199
+
200
+ def is_image_scoped(self) -> bool:
201
+ """Check if this annotation is image-scoped."""
202
+ return self.scope == 'image'
203
+
204
+ def get_created_datetime(self) -> Optional[datetime]:
205
+ """
206
+ Get the creation datetime as a datetime object.
207
+
208
+ Returns:
209
+ datetime object or None if created_at is not set
210
+ """
211
+ if self.created_at:
212
+ try:
213
+ return datetime.fromisoformat(self.created_at.replace('Z', '+00:00'))
214
+ except ValueError:
215
+ _LOGGER.warning(f"Could not parse created_at datetime: {self.created_at}")
216
+ return None
217
+
218
+ def __repr__(self) -> str:
219
+ """String representation of the annotation."""
220
+ return (f"Annotation(id='{self.id}', identifier='{self.identifier}', "
221
+ f"type='{self.annotation_type}', scope='{self.scope}', resource_id='{self.resource_id}')")