datamint 2.26.0__tar.gz → 2.28.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. {datamint-2.26.0 → datamint-2.28.0}/PKG-INFO +1 -1
  2. {datamint-2.26.0 → datamint-2.28.0}/datamint/__init__.py +7 -1
  3. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/base_api.py +7 -3
  4. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/annotationsets_api.py +10 -0
  5. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/inference_api.py +8 -0
  6. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/entity_base_api.py +5 -0
  7. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/base_entity.py +86 -71
  8. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/project.py +1 -11
  9. datamint-2.28.0/datamint/importers/__init__.py +9 -0
  10. datamint-2.28.0/datamint/importers/_common.py +86 -0
  11. datamint-2.28.0/datamint/importers/coco.py +150 -0
  12. datamint-2.28.0/datamint/importers/pascal_voc.py +153 -0
  13. datamint-2.28.0/datamint/importers/yolo.py +204 -0
  14. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/flavors/datamint_flavor.py +48 -1
  15. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/flavors/model.py +45 -0
  16. {datamint-2.26.0 → datamint-2.28.0}/pyproject.toml +2 -2
  17. {datamint-2.26.0 → datamint-2.28.0}/README.md +0 -0
  18. {datamint-2.26.0 → datamint-2.28.0}/datamint/__main__.py +0 -0
  19. {datamint-2.26.0 → datamint-2.28.0}/datamint/_repr_utils.py +0 -0
  20. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/__init__.py +0 -0
  21. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/client.py +0 -0
  22. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/dto/__init__.py +0 -0
  23. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/dto/annotation_dto.py +0 -0
  24. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/dto/save_results_options.py +0 -0
  25. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/__init__.py +0 -0
  26. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/annotations_api.py +0 -0
  27. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/channels_api.py +0 -0
  28. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/datasetsinfo_api.py +0 -0
  29. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/deploy_model_api.py +0 -0
  30. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/model_types.py +0 -0
  31. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/models_api.py +0 -0
  32. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/projects_api.py +0 -0
  33. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/resources_api.py +0 -0
  34. {datamint-2.26.0 → datamint-2.28.0}/datamint/api/endpoints/users_api.py +0 -0
  35. {datamint-2.26.0 → datamint-2.28.0}/datamint/client_cmd_tools/__init__.py +0 -0
  36. {datamint-2.26.0 → datamint-2.28.0}/datamint/client_cmd_tools/datamint_config.py +0 -0
  37. {datamint-2.26.0 → datamint-2.28.0}/datamint/client_cmd_tools/datamint_example.py +0 -0
  38. {datamint-2.26.0 → datamint-2.28.0}/datamint/client_cmd_tools/datamint_inference.py +0 -0
  39. {datamint-2.26.0 → datamint-2.28.0}/datamint/client_cmd_tools/datamint_init.py +0 -0
  40. {datamint-2.26.0 → datamint-2.28.0}/datamint/client_cmd_tools/datamint_train.py +0 -0
  41. {datamint-2.26.0 → datamint-2.28.0}/datamint/client_cmd_tools/datamint_upload.py +0 -0
  42. {datamint-2.26.0 → datamint-2.28.0}/datamint/configs.py +0 -0
  43. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/__init__.py +0 -0
  44. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/annotation_processor.py +0 -0
  45. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/base.py +0 -0
  46. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/factory.py +0 -0
  47. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/image_dataset.py +0 -0
  48. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/multiframe_dataset.py +0 -0
  49. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/sliced_dataset.py +0 -0
  50. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/sliced_video_dataset.py +0 -0
  51. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/split_result.py +0 -0
  52. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/video_dataset.py +0 -0
  53. {datamint-2.26.0 → datamint-2.28.0}/datamint/dataset/volume_dataset.py +0 -0
  54. {datamint-2.26.0 → datamint-2.28.0}/datamint/default_project.py +0 -0
  55. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/__init__.py +0 -0
  56. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotation_worklist.py +0 -0
  57. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/__init__.py +0 -0
  58. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/annotation.py +0 -0
  59. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/annotation_spec.py +0 -0
  60. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/base_geometry.py +0 -0
  61. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/base_segmentation.py +0 -0
  62. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/box_annotation.py +0 -0
  63. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/geometry.py +0 -0
  64. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/image_classification.py +0 -0
  65. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/image_segmentation.py +0 -0
  66. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/line_annotation.py +0 -0
  67. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/numeric_annotation.py +0 -0
  68. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/types.py +0 -0
  69. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/annotations/volume_segmentation.py +0 -0
  70. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/cache_manager.py +0 -0
  71. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/channel.py +0 -0
  72. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/datasetinfo.py +0 -0
  73. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/deployjob.py +0 -0
  74. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/inferencejob.py +0 -0
  75. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/project_resource_split.py +0 -0
  76. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/resource.py +0 -0
  77. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/resources/__init__.py +0 -0
  78. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/resources/dicom_resource.py +0 -0
  79. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/resources/image_resource.py +0 -0
  80. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/resources/nifti_resource.py +0 -0
  81. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/resources/video_resource.py +0 -0
  82. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/resources/volume_resource.py +0 -0
  83. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/sliced_resource.py +0 -0
  84. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/sliced_resource_base.py +0 -0
  85. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/sliced_video_resource.py +0 -0
  86. {datamint-2.26.0 → datamint-2.28.0}/datamint/entities/user.py +0 -0
  87. {datamint-2.26.0 → datamint-2.28.0}/datamint/examples/__init__.py +0 -0
  88. {datamint-2.26.0 → datamint-2.28.0}/datamint/examples/_common.py +0 -0
  89. {datamint-2.26.0 → datamint-2.28.0}/datamint/examples/_download.py +0 -0
  90. {datamint-2.26.0 → datamint-2.28.0}/datamint/examples/bccd_dataset.py +0 -0
  91. {datamint-2.26.0 → datamint-2.28.0}/datamint/examples/busi_dataset.py +0 -0
  92. {datamint-2.26.0 → datamint-2.28.0}/datamint/examples/example_projects.py +0 -0
  93. {datamint-2.26.0 → datamint-2.28.0}/datamint/examples/fracatlas_dataset.py +0 -0
  94. {datamint-2.26.0 → datamint-2.28.0}/datamint/examples/synapse_dataset.py +0 -0
  95. {datamint-2.26.0 → datamint-2.28.0}/datamint/exceptions.py +0 -0
  96. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/__init__.py +0 -0
  97. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/datamodule.py +0 -0
  98. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/__init__.py +0 -0
  99. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/base_trainer.py +0 -0
  100. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/classification_trainer.py +0 -0
  101. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/detection_trainer.py +0 -0
  102. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/__init__.py +0 -0
  103. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/base.py +0 -0
  104. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/classification_module.py +0 -0
  105. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/detection_modules/__init__.py +0 -0
  106. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/detection_modules/yolox_module.py +0 -0
  107. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/segmentation_module.py +0 -0
  108. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/__init__.py +0 -0
  109. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/deeplabv3plus.py +0 -0
  110. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/smp_module.py +0 -0
  111. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/transunet.py +0 -0
  112. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/unetpp.py +0 -0
  113. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/lightning_modules/segmentation_modules/unetrpp.py +0 -0
  114. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/seg2d_trainer.py +0 -0
  115. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/seg3d_trainer.py +0 -0
  116. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/segmentation_trainer.py +0 -0
  117. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/__init__.py +0 -0
  118. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/deeplabv3plus.py +0 -0
  119. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/efficientnetv2.py +0 -0
  120. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/nnunet/__init__.py +0 -0
  121. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/nnunet/_nnunet_trainer_bridge.py +0 -0
  122. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/nnunet/data_export.py +0 -0
  123. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/nnunet/data_import.py +0 -0
  124. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/nnunet/inference_model.py +0 -0
  125. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/nnunet/trainer.py +0 -0
  126. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/transunet.py +0 -0
  127. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/unetpp.py +0 -0
  128. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/unetrpp.py +0 -0
  129. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/specialized/yolox.py +0 -0
  130. {datamint-2.26.0 → datamint-2.28.0}/datamint/lightning/trainers/vol_seg_trainer.py +0 -0
  131. {datamint-2.26.0 → datamint-2.28.0}/datamint/logging.yaml +0 -0
  132. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/__init__.py +0 -0
  133. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/artifact/__init__.py +0 -0
  134. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/artifact/datamint_artifacts_repo.py +0 -0
  135. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/data/__init__.py +0 -0
  136. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/data/datamint_dataset.py +0 -0
  137. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/env_utils.py +0 -0
  138. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/env_vars.py +0 -0
  139. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/flavors/__init__.py +0 -0
  140. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/flavors/model_loader.py +0 -0
  141. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/flavors/model_parser.py +0 -0
  142. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/flavors/prediction_modes.py +0 -0
  143. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/flavors/prediction_router.py +0 -0
  144. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/flavors/task_type.py +0 -0
  145. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/flavors/validation.py +0 -0
  146. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/lightning/callbacks/__init__.py +0 -0
  147. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/lightning/callbacks/modelcheckpoint.py +0 -0
  148. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/models/__init__.py +0 -0
  149. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/models/datamint_model_store.py +0 -0
  150. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/models/tags.py +0 -0
  151. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/store_utils.py +0 -0
  152. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/tracking/datamint_store.py +0 -0
  153. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/tracking/default_experiment.py +0 -0
  154. {datamint-2.26.0 → datamint-2.28.0}/datamint/mlflow/tracking/fluent.py +0 -0
  155. {datamint-2.26.0 → datamint-2.28.0}/datamint/types.py +0 -0
  156. {datamint-2.26.0 → datamint-2.28.0}/datamint/utils/annotation_agreement.py +0 -0
  157. {datamint-2.26.0 → datamint-2.28.0}/datamint/utils/collection_utils.py +0 -0
  158. {datamint-2.26.0 → datamint-2.28.0}/datamint/utils/env.py +0 -0
  159. {datamint-2.26.0 → datamint-2.28.0}/datamint/utils/logging_utils.py +0 -0
  160. {datamint-2.26.0 → datamint-2.28.0}/datamint/utils/nifti_utils.py +0 -0
  161. {datamint-2.26.0 → datamint-2.28.0}/datamint/utils/torchmetrics.py +0 -0
  162. {datamint-2.26.0 → datamint-2.28.0}/datamint/utils/uncertainty.py +0 -0
  163. {datamint-2.26.0 → datamint-2.28.0}/datamint/utils/visualization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datamint
3
- Version: 2.26.0
3
+ Version: 2.28.0
4
4
  Summary: A library for interacting with the Datamint API, designed for efficient data management, processing and Deep Learning workflows.
5
5
  Requires-Python: >=3.10
6
6
  Classifier: Programming Language :: Python :: 3
@@ -15,6 +15,9 @@ if TYPE_CHECKING:
15
15
  from .dataset.image_dataset import ImageDataset as ImageDataset
16
16
  from .dataset.volume_dataset import VolumeDataset as VolumeDataset
17
17
  from .default_project import select_project as select_project
18
+ from .importers.coco import COCOImporter as COCOImporter
19
+ from .importers.pascal_voc import PascalVOCImporter as PascalVOCImporter
20
+ from .importers.yolo import YOLOImporter as YOLOImporter
18
21
  from .mlflow.flavors.validation import (
19
22
  ModelValidationError as ModelValidationError,
20
23
  ValidationIssue as ValidationIssue,
@@ -27,7 +30,7 @@ else:
27
30
 
28
31
  __getattr__, __dir__, __all__ = lazy.attach(
29
32
  __name__,
30
- submodules=['dataset', "examples"],
33
+ submodules=['dataset', "examples", "importers"],
31
34
  submod_attrs={
32
35
  "api.client": ["Api"],
33
36
  # New modular dataset classes
@@ -36,6 +39,9 @@ else:
36
39
  "mlflow.flavors.validation": ["validate_model", "ValidationReport",
37
40
  "ValidationIssue", "ModelValidationError"],
38
41
  "default_project": ["select_project"],
42
+ "importers.coco": ["COCOImporter"],
43
+ "importers.pascal_voc": ["PascalVOCImporter"],
44
+ "importers.yolo": ["YOLOImporter"],
39
45
  },
40
46
  )
41
47
 
@@ -593,6 +593,7 @@ class BaseApi:
593
593
  endpoint: str,
594
594
  return_field: str | None = None,
595
595
  limit: int | None = None,
596
+ max_page_size: int | None = None,
596
597
  **kwargs
597
598
  ) -> Generator[tuple[httpx.Response, list | dict | str], None, None]:
598
599
  """Make paginated HTTP requests, yielding each page of results.
@@ -602,6 +603,8 @@ class BaseApi:
602
603
  endpoint: API endpoint path
603
604
  return_field: Optional field name to extract from each item in the response
604
605
  limit: Optional maximum number of items to retrieve
606
+ max_page_size: Optional cap on the per-request page size, for endpoints
607
+ whose server route enforces a lower limit than the client default.
605
608
  **kwargs: Additional arguments for the request (e.g., params, json)
606
609
 
607
610
  Yields:
@@ -609,6 +612,7 @@ class BaseApi:
609
612
  """
610
613
  offset = 0
611
614
  total_fetched = 0
615
+ page_size = min(_PAGE_LIMIT, max_page_size) if max_page_size is not None else _PAGE_LIMIT
612
616
 
613
617
  use_json_pagination = method.upper() == 'POST' and 'json' in kwargs and isinstance(kwargs['json'], dict)
614
618
 
@@ -621,10 +625,10 @@ class BaseApi:
621
625
  if limit is not None and total_fetched >= limit:
622
626
  break
623
627
 
624
- page_limit = _PAGE_LIMIT
628
+ page_limit = page_size
625
629
  if limit is not None:
626
630
  remaining = limit - total_fetched
627
- page_limit = min(_PAGE_LIMIT, remaining)
631
+ page_limit = min(page_size, remaining)
628
632
 
629
633
  if use_json_pagination:
630
634
  kwargs['json']['offset'] = str(offset)
@@ -649,7 +653,7 @@ class BaseApi:
649
653
  yield response, items_to_yield
650
654
  total_fetched += len(items_to_yield)
651
655
 
652
- if len(items) < _PAGE_LIMIT:
656
+ if len(items) < page_size:
653
657
  break
654
658
 
655
659
  offset += len(items)
@@ -173,6 +173,16 @@ class AnnotationWorklistApi(CreatableEntityApi[AnnotationWorklist],
173
173
  worklist_id,
174
174
  'segmentation-group').json()
175
175
 
176
+ def get_list(self, limit: int | None = None, **kwargs) -> list[AnnotationWorklist]:
177
+ """Not supported: there is no server endpoint to list worklists across all projects.
178
+
179
+ Use :meth:`get_by_project` instead.
180
+ """
181
+ raise NotImplementedError(
182
+ "AnnotationWorklistApi.get_list()/get_all() is not supported. Use "
183
+ "get_by_project(project) to list the worklists for a specific project."
184
+ )
185
+
176
186
  def get_by_project(self, project: 'str | Project') -> list[AnnotationWorklist]:
177
187
  """Get the worklist IDs associated with a project.
178
188
 
@@ -36,6 +36,8 @@ class InferenceApi(EntityBaseApi[InferenceJob]):
36
36
  (image, frame, slice, volume).
37
37
  """
38
38
 
39
+ _max_page_size = 500 # server enforces `le=500` on the list route's `limit` query param
40
+
39
41
  def __init__(self,
40
42
  config: ApiConfig,
41
43
  client: httpx.Client | None = None,
@@ -54,6 +56,12 @@ class InferenceApi(EntityBaseApi[InferenceJob]):
54
56
  data['id'] = data.pop('job_id')
55
57
  return self._init_entity_obj(**data)
56
58
 
59
+ def _init_entity_obj(self, **kwargs) -> InferenceJob:
60
+ """Rename the server's 'job_id' to 'id' before constructing the entity."""
61
+ if 'job_id' in kwargs and 'id' not in kwargs:
62
+ kwargs['id'] = kwargs.pop('job_id')
63
+ return super()._init_entity_obj(**kwargs)
64
+
57
65
  def _build_common_payload(
58
66
  self,
59
67
  model_name: str,
@@ -139,6 +139,10 @@ class EntityBaseApi(BaseApi, Generic[T]):
139
139
  raise ItemNotFoundError(self.endpoint_base, {'id': entity_id}) from e
140
140
  raise
141
141
 
142
+ #: Optional cap on the per-request page size, for endpoints whose server
143
+ #: route enforces a lower limit than the client default.
144
+ _max_page_size: int | None = None
145
+
142
146
  def get_list(self, limit: int | None = None,
143
147
  **kwargs) -> Sequence[T]:
144
148
  """Get entities with optional filtering.
@@ -159,6 +163,7 @@ class EntityBaseApi(BaseApi, Generic[T]):
159
163
  items_gen = self._make_request_with_pagination('GET', f'/{self.endpoint_base}',
160
164
  return_field=self.endpoint_base,
161
165
  limit=limit,
166
+ max_page_size=self._max_page_size,
162
167
  **new_kwargs)
163
168
 
164
169
  all_items = []
@@ -77,53 +77,6 @@ class BaseEntityModel(BaseModel):
77
77
  if hasattr(self, field_name) and isinstance(getattr(self, field_name), str) and getattr(self, field_name) == MISSING_FIELD:
78
78
  delattr(self, field_name)
79
79
 
80
- def __getattr__(self, name: str) -> Any:
81
- """Intercept access to missing fields and trigger automatic refresh.
82
-
83
- When a field has the MISSING_FIELD sentinel value, it is deleted in __init__.
84
- This method catches subsequent attribute access attempts and automatically
85
- refreshes the entity from the server to populate all missing fields.
86
-
87
- Private attributes (e.g. ``_api``) are forwarded to Pydantic's own
88
- ``__getattr__`` so we don't break Pydantic's private-attribute handling,
89
- which we override by defining this method.
90
- """
91
- # Delegate private attributes to Pydantic's handler (manages __pydantic_private__).
92
- # Defining our own __getattr__ overrides Pydantic's, so we must forward these.
93
- if name.startswith('_'):
94
- try:
95
- return super().__getattr__(name)
96
- except AttributeError:
97
- raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
98
-
99
- # Check if this is a declared Pydantic field that was deleted (MISSING_FIELD)
100
- try:
101
- pydantic_fields = object.__getattribute__(self, '__pydantic_fields__')
102
- except AttributeError:
103
- raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
104
-
105
- if name not in pydantic_fields:
106
- raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
107
-
108
- # This is a declared field that was deleted (MISSING_FIELD sentinel).
109
- # Try to refresh from the server using _api stored in __pydantic_private__.
110
- try:
111
- private_state = object.__getattribute__(self, '__pydantic_private__') or {}
112
- api_ref = private_state.get('_api')
113
- if api_ref is not None:
114
- _LOGGER.debug(
115
- "Lazy-loading missing field '%s' on %s (id=%s) via automatic refresh",
116
- name, type(self).__name__, getattr(self, 'id', '<unknown>')
117
- )
118
- self._refresh()
119
- # After refresh, the field should be populated
120
- return object.__getattribute__(self, name)
121
- except Exception:
122
- # If refresh fails for any reason, fall through to standard error
123
- pass
124
-
125
- raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
126
-
127
80
  def asdict(self) -> dict[str, Any]:
128
81
  """Convert the entity to a dictionary, including unknown fields."""
129
82
  d = self.model_dump(warnings='none')
@@ -149,22 +102,6 @@ class BaseEntityModel(BaseModel):
149
102
  if have_to_log:
150
103
  _LOGGER.warning(f"Unknown fields {list(self.__pydantic_extra__.keys())} found in {class_name}")
151
104
 
152
- def is_attr_missing(self, attr_name: str) -> bool:
153
- """Check if a value is the MISSING_FIELD sentinel."""
154
- if attr_name not in self.__pydantic_fields__:
155
- raise AttributeError(f"Attribute '{attr_name}' not found in entity of type '{self.__class__.__name__}'")
156
- if not hasattr(self, attr_name):
157
- return True
158
- return getattr(self, attr_name) == MISSING_FIELD
159
-
160
- def has_missing_attrs(self) -> bool:
161
- """Check if the entity has any attributes that are MISSING_FIELD.
162
-
163
- Returns:
164
- True if any attribute is MISSING_FIELD, False otherwise
165
- """
166
- return any(self.is_attr_missing(attr_name) for attr_name in self.__pydantic_fields__)
167
-
168
105
 
169
106
  class BaseEntity(BaseEntityModel):
170
107
  """
@@ -195,8 +132,8 @@ class BaseEntity(BaseEntityModel):
195
132
  # nested Pydantic models are preserved as model instances rather than
196
133
  # being converted to plain dicts (as model_dump() would do).
197
134
  for field_name in updated_ent.__pydantic_fields__:
198
- if hasattr(updated_ent, field_name): # FIXME: might be always true
199
- setattr(self, field_name, getattr(updated_ent, field_name))
135
+ if updated_ent._raw_hasattr(field_name):
136
+ setattr(self, field_name, updated_ent._getraw_attr(field_name))
200
137
 
201
138
  # Also propagate any extra (unknown) fields
202
139
  if updated_ent.__pydantic_extra__:
@@ -206,6 +143,69 @@ class BaseEntity(BaseEntityModel):
206
143
 
207
144
  return self
208
145
 
146
+ def _getraw_attr(self, name: str) -> Any:
147
+ """Get the raw attribute value without triggering automatic refresh."""
148
+ return object.__getattribute__(self, name) # Pydantic has this implemented
149
+
150
+ def _raw_hasattr(self, name: str) -> bool:
151
+ try:
152
+ v = self._getraw_attr(name)
153
+ if v is MISSING_FIELD:
154
+ return False
155
+ return True
156
+ except AttributeError:
157
+ return False
158
+
159
+ def __getattr__(self, name: str) -> Any:
160
+ """Intercept access to missing fields and trigger automatic refresh.
161
+
162
+ When a field has the MISSING_FIELD sentinel value, it is deleted in __init__.
163
+ This method catches subsequent attribute access attempts and automatically
164
+ refreshes the entity from the server to populate all missing fields.
165
+
166
+ """
167
+ # Delegate private attributes to Pydantic's handler (manages __pydantic_private__).
168
+ # Defining our own __getattr__ overrides Pydantic's, so we must forward these.
169
+ if name.startswith('_'):
170
+ try:
171
+ return super().__getattr__(name) # pydantic implements __getattr__
172
+ except AttributeError:
173
+ try:
174
+ return object.__getattribute__(self, name)
175
+ except AttributeError:
176
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
177
+
178
+ # Check if this is a declared Pydantic field that was deleted (MISSING_FIELD)
179
+ try:
180
+ pydantic_fields = object.__getattribute__(self, '__pydantic_fields__')
181
+ except AttributeError:
182
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
183
+
184
+ if name not in pydantic_fields:
185
+ # Not a declared field - fall back to pydantic's extra fields (extra='allow')
186
+ pydantic_extra = object.__getattribute__(self, '__pydantic_extra__')
187
+ if pydantic_extra is not None and name in pydantic_extra:
188
+ return pydantic_extra[name]
189
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
190
+
191
+ # This is a declared field that was deleted (MISSING_FIELD sentinel).
192
+ # Try to refresh from the server using _api stored in __pydantic_private__.
193
+ try:
194
+ private_state = object.__getattribute__(self, '__pydantic_private__') or {}
195
+ api_ref = private_state.get('_api')
196
+ if api_ref is not None:
197
+ _LOGGER.debug("Refreshing %s from server since %s is missing",
198
+ type(self).__name__, name)
199
+ self._refresh()
200
+ # After refresh, the field should be populated. If not, raise AttributeError
201
+ if self._raw_hasattr(name):
202
+ return self._getraw_attr(name)
203
+ except Exception as e:
204
+ # If refresh fails for any reason, fall through to standard error
205
+ raise
206
+
207
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
208
+
209
209
  def _ensure_attr(self, attr_name: str) -> None:
210
210
  """Ensure that a given attribute is not MISSING_FIELD, refreshing if necessary.
211
211
 
@@ -279,7 +279,7 @@ class BaseEntity(BaseEntityModel):
279
279
  # Try to get from cache
280
280
  img_data = None
281
281
  should_load_from_cache, should_save_to_cache = self._resolve_cache_mode(use_cache)
282
-
282
+
283
283
  if should_load_from_cache:
284
284
  img_data = cache_manager.get(self.id, data_key, version_info)
285
285
 
@@ -287,11 +287,10 @@ class BaseEntity(BaseEntityModel):
287
287
  # Cache miss - fetch from server
288
288
  if should_save_to_cache and save_path:
289
289
  # Download directly to save_path, register location in cache metadata
290
- _LOGGER.debug("Downloading to save_path: %s", save_path)
291
290
  Path(save_path).parent.mkdir(parents=True, exist_ok=True)
292
-
291
+
293
292
  img_data = download_callback(save_path)
294
-
293
+
295
294
  # Register save_path in cache metadata (no file duplication)
296
295
  cache_manager.register_file_location(
297
296
  self.id, data_key, save_path, version_info
@@ -300,9 +299,9 @@ class BaseEntity(BaseEntityModel):
300
299
  # No save_path - download to cache directory
301
300
  cache_path = cache_manager.get_expected_path(self.id, data_key)
302
301
  _LOGGER.debug("Downloading to cache: %s", cache_path)
303
-
302
+
304
303
  img_data = download_callback(str(cache_path))
305
-
304
+
306
305
  # Register in cache metadata
307
306
  cache_manager.set(self.id, data_key, img_data, version_info)
308
307
  else:
@@ -322,3 +321,19 @@ class BaseEntity(BaseEntityModel):
322
321
  f.write(img_data)
323
322
 
324
323
  return img_data
324
+
325
+ def is_attr_missing(self, attr_name: str) -> bool:
326
+ """Check if a value is the MISSING_FIELD sentinel."""
327
+ if attr_name not in self.__pydantic_fields__:
328
+ raise AttributeError(f"Attribute '{attr_name}' not found in entity of type '{self.__class__.__name__}'")
329
+ if not self._raw_hasattr(attr_name):
330
+ return True
331
+ return getattr(self, attr_name) == MISSING_FIELD
332
+
333
+ def has_missing_attrs(self) -> bool:
334
+ """Check if the entity has any attributes that are MISSING_FIELD.
335
+
336
+ Returns:
337
+ True if any attribute is MISSING_FIELD, False otherwise
338
+ """
339
+ return any(self.is_attr_missing(attr_name) for attr_name in self.__pydantic_fields__)
@@ -45,7 +45,6 @@ class Project(BaseEntity):
45
45
  closed_resources_count: Number of resources marked as closed/completed
46
46
  resources_to_annotate_count: Number of resources still needing annotation
47
47
  annotators: List of annotators assigned to this project
48
- pinned_metrics: Metric names pinned for display for this project
49
48
  """
50
49
  id: str
51
50
  name: str
@@ -55,18 +54,9 @@ class Project(BaseEntity):
55
54
  archived: bool
56
55
  resource_count: int
57
56
  description: str | None
58
- registered_model: Any | None = Field(default=MISSING_FIELD)
59
- ai_model_id: str | None = Field(default=MISSING_FIELD)
60
- closed_resources_count: int = Field(default=MISSING_FIELD)
61
- resources_to_annotate_count: int = Field(default=MISSING_FIELD)
57
+ archived: bool
62
58
  most_recent_experiment: str | None = Field(default=MISSING_FIELD)
63
59
  annotators: list[_ProjectAnnotatorInfo] = Field(default=MISSING_FIELD)
64
- archived_on: str | None = Field(default=MISSING_FIELD)
65
- archived_by: str | None = Field(default=MISSING_FIELD)
66
- is_active_learning: bool = Field(default=MISSING_FIELD)
67
- two_up_display: bool = Field(default=MISSING_FIELD)
68
- require_review: bool = Field(default=MISSING_FIELD)
69
- pinned_metrics: list[str] | None = Field(default=MISSING_FIELD)
70
60
 
71
61
  _api: 'ProjectsApi' = PrivateAttr()
72
62
 
@@ -0,0 +1,9 @@
1
+ from .coco import COCOBox, COCOImporter, COCOImportResult, COCOParseResult, COCOSample
2
+ from .pascal_voc import PascalVOCBox, PascalVOCImporter, PascalVOCImportResult, PascalVOCParseResult, PascalVOCSample
3
+ from .yolo import YOLOBox, YOLOImporter, YOLOImportResult, YOLOParseResult, YOLOSample
4
+
5
+ __all__ = [
6
+ 'COCOImporter', 'COCOParseResult', 'COCOImportResult', 'COCOSample', 'COCOBox',
7
+ 'PascalVOCImporter', 'PascalVOCParseResult', 'PascalVOCImportResult', 'PascalVOCSample', 'PascalVOCBox',
8
+ 'YOLOImporter', 'YOLOParseResult', 'YOLOImportResult', 'YOLOSample', 'YOLOBox',
9
+ ]
@@ -0,0 +1,86 @@
1
+ import logging
2
+ from typing import Callable, Literal, Sequence
3
+
4
+ from tqdm.auto import tqdm
5
+
6
+ from datamint import Api
7
+ from datamint.entities import Project
8
+
9
+ _LOGGER = logging.getLogger(__name__)
10
+
11
+
12
+ def import_boxes_to_project(result,
13
+ project: Project | str,
14
+ api: Api | None,
15
+ *,
16
+ box_points: Callable[[object], tuple[tuple[float, float], tuple[float, float]]],
17
+ source_label: str,
18
+ tags: Sequence[str] | None,
19
+ imported_from: str,
20
+ on_error: Literal['raise', 'skip'],
21
+ progress_bar: bool,
22
+ result_cls: type):
23
+ """Shared upload loop behind every ``*Importer.import_to_project()``.
24
+
25
+ ``result`` is a parse result with ``samples``, ``missing_images``, and
26
+ ``unsupported_annotations`` attributes. ``box_points`` maps a format's box
27
+ dataclass to a ``(point1, point2)`` pair for ``add_box_annotation``.
28
+
29
+ """
30
+ api = api or Api()
31
+
32
+ if result.missing_images:
33
+ _LOGGER.warning(f'{len(result.missing_images)} image(s) referenced in {source_label} '
34
+ f'were not found on disk and will be skipped.')
35
+ if result.unsupported_annotations:
36
+ _LOGGER.warning(f'{result.unsupported_annotations} annotation(s) in {source_label} use an '
37
+ f'unsupported annotation type (e.g. polygon/segmentation) and were not '
38
+ f'imported; only bounding boxes are supported.')
39
+
40
+ uploaded = api.resources.upload_resources(
41
+ [str(s.image_path) for s in result.samples],
42
+ tags=tags,
43
+ publish_to=project,
44
+ on_error=on_error,
45
+ progress_bar=progress_bar,
46
+ )
47
+
48
+ resource_ids: list[str] = []
49
+ errors: list[tuple[str, Exception]] = []
50
+ n_boxes_uploaded = 0
51
+
52
+ iterator = zip(result.samples, uploaded)
53
+ if progress_bar:
54
+ iterator = tqdm(iterator, total=len(result.samples), desc='Uploading annotations')
55
+
56
+ for sample, resource_id in iterator:
57
+ if isinstance(resource_id, Exception):
58
+ errors.append((sample.file_name, resource_id))
59
+ continue
60
+ resource_ids.append(resource_id)
61
+
62
+ for box in sample.boxes:
63
+ point1, point2 = box_points(box)
64
+ try:
65
+ api.annotations.add_box_annotation(
66
+ point1=point1,
67
+ point2=point2,
68
+ resource=resource_id,
69
+ identifier=box.label,
70
+ imported_from=imported_from,
71
+ )
72
+ n_boxes_uploaded += 1
73
+ except (KeyboardInterrupt, SystemExit):
74
+ raise
75
+ except Exception as e:
76
+ if on_error == 'raise':
77
+ raise
78
+ errors.append((sample.file_name, e))
79
+
80
+ return result_cls(
81
+ project=project,
82
+ resource_ids=resource_ids,
83
+ n_images_uploaded=len(resource_ids),
84
+ n_boxes_uploaded=n_boxes_uploaded,
85
+ errors=errors,
86
+ )
@@ -0,0 +1,150 @@
1
+ import json
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+ from typing import Literal, Sequence
5
+
6
+ from datamint import Api
7
+ from datamint.entities import Project
8
+
9
+ from . import _common
10
+
11
+
12
+ @dataclass
13
+ class COCOBox:
14
+ label: str
15
+ x: float
16
+ y: float
17
+ width: float
18
+ height: float
19
+
20
+
21
+ @dataclass
22
+ class COCOSample:
23
+ image_path: Path
24
+ file_name: str
25
+ boxes: list[COCOBox] = field(default_factory=list)
26
+
27
+
28
+ @dataclass
29
+ class COCOParseResult:
30
+ samples: list[COCOSample]
31
+ class_names: list[str]
32
+ missing_images: list[str]
33
+ unsupported_annotations: int
34
+
35
+ @property
36
+ def num_images(self) -> int:
37
+ return len(self.samples)
38
+
39
+ @property
40
+ def num_boxes(self) -> int:
41
+ return sum(len(s.boxes) for s in self.samples)
42
+
43
+
44
+ @dataclass
45
+ class COCOImportResult:
46
+ project: Project | str
47
+ resource_ids: list[str]
48
+ n_images_uploaded: int
49
+ n_boxes_uploaded: int
50
+ errors: list[tuple[str, Exception]] = field(default_factory=list)
51
+
52
+
53
+ class COCOImporter:
54
+ """Parse a COCO-format annotations file and upload it to a Datamint project. """
55
+
56
+ def __init__(self, annotations_file: str | Path, images_dir: str | Path | None = None):
57
+ self.annotations_file = Path(annotations_file)
58
+ self.images_dir = Path(images_dir) if images_dir is not None else self.annotations_file.parent
59
+ self._result: COCOParseResult | None = None
60
+
61
+ def parse(self, force: bool = False) -> COCOParseResult:
62
+ """Read and validate the COCO JSON file.
63
+
64
+ Cached after the first call; pass ``force=True`` to reparse.
65
+
66
+ Raises:
67
+ ValueError: If the file is structurally invalid (missing required
68
+ keys, or an annotation references an unknown category/image id).
69
+ """
70
+ if self._result is not None and not force:
71
+ return self._result
72
+
73
+ with open(self.annotations_file) as f:
74
+ data = json.load(f)
75
+
76
+ for key in ('images', 'annotations', 'categories'):
77
+ if key not in data:
78
+ raise ValueError(f"Invalid COCO file: missing required key '{key}'.")
79
+
80
+ categories = {cat['id']: cat['name'] for cat in data['categories']}
81
+
82
+ images_by_id: dict[int, tuple[str, Path]] = {}
83
+ missing_images: list[str] = []
84
+ samples_by_id: dict[int, COCOSample] = {}
85
+ for img in data['images']:
86
+ file_name = img['file_name']
87
+ image_path = self.images_dir / file_name
88
+ images_by_id[img['id']] = (file_name, image_path)
89
+ if not image_path.exists():
90
+ missing_images.append(file_name)
91
+ continue
92
+ samples_by_id[img['id']] = COCOSample(image_path=image_path, file_name=file_name)
93
+
94
+ used_class_names: set[str] = set()
95
+ unsupported_annotations = 0
96
+ for ann in data['annotations']:
97
+ image_id = ann['image_id']
98
+ if image_id not in images_by_id:
99
+ raise ValueError(f"Annotation {ann.get('id')} references unknown image_id {image_id}.")
100
+ if image_id not in samples_by_id:
101
+ continue # image file missing on disk, already recorded above
102
+
103
+ category_id = ann['category_id']
104
+ if category_id not in categories:
105
+ raise ValueError(f"Annotation {ann.get('id')} references unknown category_id {category_id}.")
106
+
107
+ bbox = ann.get('bbox')
108
+ if bbox is None:
109
+ # COCO allows annotations without bounding boxes (e.g., segmentation-only annotations)
110
+ if ann.get('segmentation'):
111
+ unsupported_annotations += 1
112
+ continue
113
+
114
+ x, y, width, height = bbox
115
+ label = categories[category_id]
116
+ samples_by_id[image_id].boxes.append(COCOBox(label=label, x=x, y=y, width=width, height=height))
117
+ used_class_names.add(label)
118
+
119
+ self._result = COCOParseResult(
120
+ samples=list(samples_by_id.values()),
121
+ class_names=sorted(used_class_names),
122
+ missing_images=missing_images,
123
+ unsupported_annotations=unsupported_annotations,
124
+ )
125
+ return self._result
126
+
127
+ def import_to_project(self,
128
+ project: Project | str,
129
+ api: Api | None = None,
130
+ *,
131
+ tags: Sequence[str] | None = None,
132
+ imported_from: str = 'coco-import',
133
+ on_error: Literal['raise', 'skip'] = 'raise',
134
+ progress_bar: bool = True) -> COCOImportResult:
135
+ """Upload the parsed images and box annotations to a Datamint project.
136
+
137
+ Calls :meth:`parse` first (reusing the cached result if already called).
138
+ ``api`` defaults to a new :class:`~datamint.api.client.Api` instance if not given.
139
+ """
140
+ result = self.parse()
141
+ return _common.import_boxes_to_project(
142
+ result, project, api,
143
+ box_points=lambda b: ((b.x, b.y), (b.x + b.width, b.y + b.height)),
144
+ source_label=str(self.annotations_file),
145
+ tags=tags,
146
+ imported_from=imported_from,
147
+ on_error=on_error,
148
+ progress_bar=progress_bar,
149
+ result_cls=COCOImportResult,
150
+ )