lamindb 1.4.0__py3-none-any.whl → 1.5.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.
Files changed (55) hide show
  1. lamindb/__init__.py +52 -36
  2. lamindb/_finish.py +17 -10
  3. lamindb/_tracked.py +1 -1
  4. lamindb/base/__init__.py +3 -1
  5. lamindb/base/fields.py +40 -22
  6. lamindb/base/ids.py +1 -94
  7. lamindb/base/types.py +2 -0
  8. lamindb/base/uids.py +117 -0
  9. lamindb/core/_context.py +177 -89
  10. lamindb/core/_settings.py +38 -25
  11. lamindb/core/datasets/__init__.py +11 -4
  12. lamindb/core/datasets/_core.py +5 -5
  13. lamindb/core/datasets/_small.py +0 -93
  14. lamindb/core/datasets/mini_immuno.py +172 -0
  15. lamindb/core/loaders.py +1 -1
  16. lamindb/core/storage/_backed_access.py +100 -6
  17. lamindb/core/storage/_polars_lazy_df.py +51 -0
  18. lamindb/core/storage/_pyarrow_dataset.py +15 -30
  19. lamindb/core/storage/objects.py +6 -0
  20. lamindb/core/subsettings/__init__.py +2 -0
  21. lamindb/core/subsettings/_annotation_settings.py +11 -0
  22. lamindb/curators/__init__.py +7 -3349
  23. lamindb/curators/_legacy.py +2056 -0
  24. lamindb/curators/core.py +1546 -0
  25. lamindb/errors.py +11 -0
  26. lamindb/examples/__init__.py +27 -0
  27. lamindb/examples/schemas/__init__.py +12 -0
  28. lamindb/examples/schemas/_anndata.py +25 -0
  29. lamindb/examples/schemas/_simple.py +19 -0
  30. lamindb/integrations/_vitessce.py +8 -5
  31. lamindb/migrations/0091_alter_featurevalue_options_alter_space_options_and_more.py +24 -0
  32. lamindb/migrations/0092_alter_artifactfeaturevalue_artifact_and_more.py +75 -0
  33. lamindb/models/__init__.py +4 -1
  34. lamindb/models/_describe.py +21 -4
  35. lamindb/models/_feature_manager.py +365 -286
  36. lamindb/models/_label_manager.py +8 -2
  37. lamindb/models/artifact.py +173 -95
  38. lamindb/models/artifact_set.py +122 -0
  39. lamindb/models/collection.py +73 -52
  40. lamindb/models/core.py +1 -1
  41. lamindb/models/feature.py +51 -17
  42. lamindb/models/has_parents.py +2 -2
  43. lamindb/models/project.py +1 -1
  44. lamindb/models/query_manager.py +221 -22
  45. lamindb/models/query_set.py +245 -171
  46. lamindb/models/record.py +62 -243
  47. lamindb/models/run.py +4 -4
  48. lamindb/models/save.py +8 -2
  49. lamindb/models/schema.py +458 -181
  50. lamindb/models/transform.py +2 -2
  51. lamindb/models/ulabel.py +8 -5
  52. {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/METADATA +6 -6
  53. {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/RECORD +55 -42
  54. {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/LICENSE +0 -0
  55. {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/WHEEL +0 -0
lamindb/models/record.py CHANGED
@@ -5,7 +5,6 @@ import inspect
5
5
  import re
6
6
  import sys
7
7
  from collections import defaultdict
8
- from functools import reduce
9
8
  from itertools import chain
10
9
  from pathlib import PurePosixPath
11
10
  from typing import (
@@ -21,36 +20,15 @@ from typing import (
21
20
  import dj_database_url
22
21
  import lamindb_setup as ln_setup
23
22
  from django.core.exceptions import ValidationError as DjangoValidationError
24
- from django.db import IntegrityError, connections, models, transaction
25
- from django.db.models import (
26
- CASCADE,
27
- PROTECT,
28
- Field,
29
- IntegerField,
30
- Manager,
31
- Q,
32
- QuerySet,
33
- Value,
34
- )
23
+ from django.db import IntegrityError, ProgrammingError, connections, models, transaction
24
+ from django.db.models import CASCADE, PROTECT, Field, Manager, QuerySet
35
25
  from django.db.models.base import ModelBase
36
26
  from django.db.models.fields.related import (
37
27
  ManyToManyField,
38
28
  ManyToManyRel,
39
29
  ManyToOneRel,
40
30
  )
41
- from django.db.models.functions import Cast, Coalesce
42
- from django.db.models.lookups import (
43
- Contains,
44
- Exact,
45
- IContains,
46
- IExact,
47
- IRegex,
48
- IStartsWith,
49
- Regex,
50
- StartsWith,
51
- )
52
31
  from lamin_utils import colors, logger
53
- from lamin_utils._lookup import Lookup
54
32
  from lamindb_setup import settings as setup_settings
55
33
  from lamindb_setup._connect_instance import (
56
34
  get_owner_name_from_identifier,
@@ -58,27 +36,27 @@ from lamindb_setup._connect_instance import (
58
36
  update_db_using_local,
59
37
  )
60
38
  from lamindb_setup.core._docs import doc_args
61
- from lamindb_setup.core._hub_core import access_db, connect_instance_hub
39
+ from lamindb_setup.core._hub_core import connect_instance_hub
62
40
  from lamindb_setup.core._settings_store import instance_settings_file
63
- from lamindb_setup.core.django import db_token_manager
41
+ from lamindb_setup.core.django import DBToken, db_token_manager
64
42
  from lamindb_setup.core.upath import extract_suffix_from_path
65
43
 
66
- from lamindb.base.fields import (
44
+ from ..base.fields import (
67
45
  CharField,
68
46
  DateTimeField,
69
47
  ForeignKey,
70
48
  JSONField,
71
- TextField,
72
49
  )
73
- from lamindb.base.types import FieldAttr, StrField
74
- from lamindb.errors import FieldValidationError
75
-
50
+ from ..base.types import FieldAttr, StrField
76
51
  from ..errors import (
52
+ FieldValidationError,
77
53
  InvalidArgument,
54
+ NoWriteAccess,
78
55
  RecordNameChangeIntegrityError,
79
56
  ValidationError,
80
57
  )
81
58
  from ._is_versioned import IsVersioned
59
+ from .query_manager import QueryManager, _lookup, _search
82
60
 
83
61
  if TYPE_CHECKING:
84
62
  from datetime import datetime
@@ -183,13 +161,19 @@ def init_self_from_db(self: Record, existing_record: Record):
183
161
 
184
162
  def update_attributes(record: Record, attributes: dict[str, str]):
185
163
  for key, value in attributes.items():
186
- if (
187
- getattr(record, key) != value
188
- and value is not None
189
- and key not in {"dtype", "otype", "_aux"}
190
- ):
191
- logger.warning(f"updated {key} from {getattr(record, key)} to {value}")
192
- setattr(record, key, value)
164
+ if getattr(record, key) != value and value is not None:
165
+ if key not in {"uid", "dtype", "otype", "hash"}:
166
+ logger.warning(f"updated {key} from {getattr(record, key)} to {value}")
167
+ setattr(record, key, value)
168
+ else:
169
+ hash_message = (
170
+ "recomputing on .save()"
171
+ if key == "hash"
172
+ else f"keeping {getattr(record, key)}"
173
+ )
174
+ logger.warning(
175
+ f"ignoring tentative value {value} for {key}, {hash_message}"
176
+ )
193
177
 
194
178
 
195
179
  def validate_literal_fields(record: Record, kwargs) -> None:
@@ -277,9 +261,12 @@ def validate_fields(record: Record, kwargs):
277
261
  "uid"
278
262
  ).max_length # triggers FieldDoesNotExist
279
263
  if len(kwargs["uid"]) != uid_max_length: # triggers KeyError
280
- raise ValidationError(
281
- f"`uid` must be exactly {uid_max_length} characters long, got {len(kwargs['uid'])}."
282
- )
264
+ if not (
265
+ record.__class__ is Schema and len(kwargs["uid"]) == 16
266
+ ): # no error for schema
267
+ raise ValidationError(
268
+ f"`uid` must be exactly {uid_max_length} characters long, got {len(kwargs['uid'])}."
269
+ )
283
270
  # validate is_type
284
271
  if "is_type" in kwargs and "name" in kwargs and kwargs["is_type"]:
285
272
  if kwargs["name"].endswith("s"):
@@ -412,36 +399,13 @@ class Registry(ModelBase):
412
399
  def __repr__(cls) -> str:
413
400
  return registry_repr(cls)
414
401
 
402
+ @doc_args(_lookup.__doc__)
415
403
  def lookup(
416
404
  cls,
417
405
  field: StrField | None = None,
418
406
  return_field: StrField | None = None,
419
407
  ) -> NamedTuple:
420
- """Return an auto-complete object for a field.
421
-
422
- Args:
423
- field: The field to look up the values for. Defaults to first string field.
424
- return_field: The field to return. If `None`, returns the whole record.
425
-
426
- Returns:
427
- A `NamedTuple` of lookup information of the field values with a
428
- dictionary converter.
429
-
430
- See Also:
431
- :meth:`~lamindb.models.Record.search`
432
-
433
- Examples:
434
- >>> import bionty as bt
435
- >>> bt.settings.organism = "human"
436
- >>> bt.Gene.from_source(symbol="ADGB-DT").save()
437
- >>> lookup = bt.Gene.lookup()
438
- >>> lookup.adgb_dt
439
- >>> lookup_dict = lookup.dict()
440
- >>> lookup_dict['ADGB-DT']
441
- >>> lookup_by_ensembl_id = bt.Gene.lookup(field="ensembl_gene_id")
442
- >>> genes.ensg00000002745
443
- >>> lookup_return_symbols = bt.Gene.lookup(field="ensembl_gene_id", return_field="symbol")
444
- """
408
+ """{}""" # noqa: D415
445
409
  return _lookup(cls=cls, field=field, return_field=return_field)
446
410
 
447
411
  def filter(cls, *queries, **expressions) -> QuerySet:
@@ -541,6 +505,7 @@ class Registry(ModelBase):
541
505
  query_set = query_set.order_by("-updated_at")
542
506
  return query_set[:limit].df(include=include, features=features)
543
507
 
508
+ @doc_args(_search.__doc__)
544
509
  def search(
545
510
  cls,
546
511
  string: str,
@@ -549,27 +514,7 @@ class Registry(ModelBase):
549
514
  limit: int | None = 20,
550
515
  case_sensitive: bool = False,
551
516
  ) -> QuerySet:
552
- """Search.
553
-
554
- Args:
555
- string: The input string to match against the field ontology values.
556
- field: The field or fields to search. Search all string fields by default.
557
- limit: Maximum amount of top results to return.
558
- case_sensitive: Whether the match is case sensitive.
559
-
560
- Returns:
561
- A sorted `DataFrame` of search results with a score in column `score`.
562
- If `return_queryset` is `True`. `QuerySet`.
563
-
564
- See Also:
565
- :meth:`~lamindb.models.Record.filter`
566
- :meth:`~lamindb.models.Record.lookup`
567
-
568
- Examples:
569
- >>> ulabels = ln.ULabel.from_values(["ULabel1", "ULabel2", "ULabel3"], field="name")
570
- >>> ln.save(ulabels)
571
- >>> ln.ULabel.search("ULabel2")
572
- """
517
+ """{}""" # noqa: D415
573
518
  return _search(
574
519
  cls=cls,
575
520
  string=string,
@@ -597,6 +542,9 @@ class Registry(ModelBase):
597
542
  """
598
543
  from .query_set import QuerySet
599
544
 
545
+ # connection already established
546
+ if instance in connections:
547
+ return QuerySet(model=cls, using=instance)
600
548
  # we're in the default instance
601
549
  if instance is None or instance == "default":
602
550
  return QuerySet(model=cls, using=None)
@@ -631,7 +579,7 @@ class Registry(ModelBase):
631
579
  iresult["fine_grained_access"] and iresult["db_permissions"] == "jwt"
632
580
  )
633
581
  # access_db can take both: the dict from connect_instance_hub and isettings
634
- into_access_db = iresult
582
+ into_db_token = iresult
635
583
  else:
636
584
  isettings = load_instance_settings(settings_file)
637
585
  source_modules = isettings.modules
@@ -644,10 +592,10 @@ class Registry(ModelBase):
644
592
  isettings._fine_grained_access and isettings._db_permissions == "jwt"
645
593
  )
646
594
  # access_db can take both: the dict from connect_instance_hub and isettings
647
- into_access_db = isettings
595
+ into_db_token = isettings
648
596
 
649
597
  target_modules = setup_settings.instance.modules
650
- if not (missing_members := source_modules - target_modules):
598
+ if missing_members := source_modules - target_modules:
651
599
  logger.warning(
652
600
  f"source modules has additional modules: {missing_members}\n"
653
601
  "consider mounting these registry modules to transfer all metadata"
@@ -655,7 +603,7 @@ class Registry(ModelBase):
655
603
 
656
604
  add_db_connection(db, instance)
657
605
  if is_fine_grained_access:
658
- db_token = access_db(into_access_db)
606
+ db_token = DBToken(into_db_token)
659
607
  db_token_manager.set(db_token, instance)
660
608
  return QuerySet(model=cls, using=instance)
661
609
 
@@ -697,15 +645,19 @@ class BasicRecord(models.Model, metaclass=Registry):
697
645
  It's mainly used for LinkORMs and similar.
698
646
  """
699
647
 
648
+ objects = QueryManager()
649
+
700
650
  class Meta:
701
651
  abstract = True
652
+ base_manager_name = "objects"
702
653
 
703
654
  def __init__(self, *args, **kwargs):
704
655
  skip_validation = kwargs.pop("_skip_validation", False)
705
656
  if not args:
706
657
  if (
707
658
  issubclass(self.__class__, Record)
708
- and not self.__class__.__name__ == "Storage"
659
+ and self.__class__.__name__
660
+ not in {"Storage", "ULabel", "Feature", "Schema", "Param"}
709
661
  # do not save bionty entities in restricted spaces by default
710
662
  and self.__class__.__module__ != "bionty.models"
711
663
  ):
@@ -719,7 +671,6 @@ class BasicRecord(models.Model, metaclass=Registry):
719
671
  from ..core._settings import settings
720
672
  from .can_curate import CanCurate
721
673
  from .collection import Collection
722
- from .schema import Schema
723
674
  from .transform import Transform
724
675
 
725
676
  validate_fields(self, kwargs)
@@ -763,11 +714,6 @@ class BasicRecord(models.Model, metaclass=Registry):
763
714
  f"returning existing {self.__class__.__name__} record with same"
764
715
  f" {name_field}{version_comment}: '{kwargs[name_field]}'"
765
716
  )
766
- if isinstance(self, Schema):
767
- if existing_record.hash != kwargs["hash"]:
768
- logger.warning(
769
- f"You're updating schema {existing_record.uid}, which might already have been used to validate datasets. Be careful."
770
- )
771
717
  init_self_from_db(self, existing_record)
772
718
  update_attributes(self, kwargs)
773
719
  return None
@@ -840,20 +786,33 @@ class BasicRecord(models.Model, metaclass=Registry):
840
786
  # save unversioned record
841
787
  else:
842
788
  super().save(*args, **kwargs)
843
- except IntegrityError as e:
789
+ except (IntegrityError, ProgrammingError) as e:
844
790
  error_msg = str(e)
845
791
  # two possible error messages for hash duplication
846
792
  # "duplicate key value violates unique constraint"
847
793
  # "UNIQUE constraint failed"
848
794
  if (
849
- "UNIQUE constraint failed" in error_msg
850
- or "duplicate key value violates unique constraint" in error_msg
851
- ) and "hash" in error_msg:
795
+ isinstance(e, IntegrityError)
796
+ and "hash" in error_msg
797
+ and (
798
+ "UNIQUE constraint failed" in error_msg
799
+ or "duplicate key value violates unique constraint" in error_msg
800
+ )
801
+ ):
852
802
  pre_existing_record = self.__class__.get(hash=self.hash)
853
803
  logger.warning(
854
804
  f"returning {self.__class__.__name__.lower()} with same hash: {pre_existing_record}"
855
805
  )
856
806
  init_self_from_db(self, pre_existing_record)
807
+ elif (
808
+ isinstance(e, ProgrammingError)
809
+ and hasattr(self, "space")
810
+ and "new row violates row-level security policy" in error_msg
811
+ ):
812
+ raise NoWriteAccess(
813
+ f"You’re not allowed to write to the space '{self.space.name}'.\n"
814
+ "Please contact an administrator of the space if you need write access."
815
+ ) from None
857
816
  else:
858
817
  raise
859
818
  # call the below in case a user makes more updates to the record
@@ -929,7 +888,7 @@ class BasicRecord(models.Model, metaclass=Registry):
929
888
 
930
889
 
931
890
  class Space(BasicRecord):
932
- """Spaces.
891
+ """Spaces to restrict access to records to specific users or teams.
933
892
 
934
893
  You can use spaces to restrict access to records within an instance.
935
894
 
@@ -1110,146 +1069,6 @@ def _get_record_kwargs(record_class) -> list[tuple[str, str]]:
1110
1069
  return []
1111
1070
 
1112
1071
 
1113
- def _search(
1114
- cls,
1115
- string: str,
1116
- *,
1117
- field: StrField | list[StrField] | None = None,
1118
- limit: int | None = 20,
1119
- case_sensitive: bool = False,
1120
- truncate_string: bool = False,
1121
- ) -> QuerySet:
1122
- if string is None:
1123
- raise ValueError("Cannot search for None value! Please pass a valid string.")
1124
-
1125
- input_queryset = (
1126
- cls.all() if isinstance(cls, (QuerySet, Manager)) else cls.objects.all()
1127
- )
1128
- registry = input_queryset.model
1129
- name_field = getattr(registry, "_name_field", "name")
1130
- if field is None:
1131
- fields = [
1132
- field.name
1133
- for field in registry._meta.fields
1134
- if field.get_internal_type() in {"CharField", "TextField"}
1135
- ]
1136
- else:
1137
- if not isinstance(field, list):
1138
- fields_input = [field]
1139
- else:
1140
- fields_input = field
1141
- fields = []
1142
- for field in fields_input:
1143
- if not isinstance(field, str):
1144
- try:
1145
- fields.append(field.field.name)
1146
- except AttributeError as error:
1147
- raise TypeError(
1148
- "Please pass a Record string field, e.g., `CellType.name`!"
1149
- ) from error
1150
- else:
1151
- fields.append(field)
1152
-
1153
- if truncate_string:
1154
- if (len_string := len(string)) > 5:
1155
- n_80_pct = int(len_string * 0.8)
1156
- string = string[:n_80_pct]
1157
-
1158
- string = string.strip()
1159
- string_escape = re.escape(string)
1160
-
1161
- exact_lookup = Exact if case_sensitive else IExact
1162
- regex_lookup = Regex if case_sensitive else IRegex
1163
- contains_lookup = Contains if case_sensitive else IContains
1164
-
1165
- ranks = []
1166
- contains_filters = []
1167
- for field in fields:
1168
- field_expr = Coalesce(
1169
- Cast(field, output_field=TextField()),
1170
- Value(""),
1171
- output_field=TextField(),
1172
- )
1173
- # exact rank
1174
- exact_expr = exact_lookup(field_expr, string)
1175
- exact_rank = Cast(exact_expr, output_field=IntegerField()) * 200
1176
- ranks.append(exact_rank)
1177
- # exact synonym
1178
- synonym_expr = regex_lookup(field_expr, rf"(?:^|.*\|){string_escape}(?:\|.*|$)")
1179
- synonym_rank = Cast(synonym_expr, output_field=IntegerField()) * 200
1180
- ranks.append(synonym_rank)
1181
- # match as sub-phrase
1182
- sub_expr = regex_lookup(
1183
- field_expr, rf"(?:^|.*[ \|\.,;:]){string_escape}(?:[ \|\.,;:].*|$)"
1184
- )
1185
- sub_rank = Cast(sub_expr, output_field=IntegerField()) * 10
1186
- ranks.append(sub_rank)
1187
- # startswith and avoid matching string with " " on the right
1188
- # mostly for truncated
1189
- startswith_expr = regex_lookup(
1190
- field_expr, rf"(?:^|.*\|){string_escape}[^ ]*(?:\|.*|$)"
1191
- )
1192
- startswith_rank = Cast(startswith_expr, output_field=IntegerField()) * 8
1193
- ranks.append(startswith_rank)
1194
- # match as sub-phrase from the left, mostly for truncated
1195
- right_expr = regex_lookup(field_expr, rf"(?:^|.*[ \|]){string_escape}.*")
1196
- right_rank = Cast(right_expr, output_field=IntegerField()) * 2
1197
- ranks.append(right_rank)
1198
- # match as sub-phrase from the right
1199
- left_expr = regex_lookup(field_expr, rf".*{string_escape}(?:$|[ \|\.,;:].*)")
1200
- left_rank = Cast(left_expr, output_field=IntegerField()) * 2
1201
- ranks.append(left_rank)
1202
- # simple contains filter
1203
- contains_expr = contains_lookup(field_expr, string)
1204
- contains_filter = Q(contains_expr)
1205
- contains_filters.append(contains_filter)
1206
- # also rank by contains
1207
- contains_rank = Cast(contains_expr, output_field=IntegerField())
1208
- ranks.append(contains_rank)
1209
- # additional rule for truncated strings
1210
- # weight matches from the beginning of the string higher
1211
- # sometimes whole words get truncated and startswith_expr is not enough
1212
- if truncate_string and field == name_field:
1213
- startswith_lookup = StartsWith if case_sensitive else IStartsWith
1214
- name_startswith_expr = startswith_lookup(field_expr, string)
1215
- name_startswith_rank = (
1216
- Cast(name_startswith_expr, output_field=IntegerField()) * 2
1217
- )
1218
- ranks.append(name_startswith_rank)
1219
-
1220
- ranked_queryset = (
1221
- input_queryset.filter(reduce(lambda a, b: a | b, contains_filters))
1222
- .alias(rank=sum(ranks))
1223
- .order_by("-rank")
1224
- )
1225
-
1226
- return ranked_queryset[:limit]
1227
-
1228
-
1229
- def _lookup(
1230
- cls,
1231
- field: StrField | None = None,
1232
- return_field: StrField | None = None,
1233
- using_key: str | None = None,
1234
- ) -> NamedTuple:
1235
- """{}""" # noqa: D415
1236
- queryset = cls.all() if isinstance(cls, (QuerySet, Manager)) else cls.objects.all()
1237
- field = get_name_field(registry=queryset.model, field=field)
1238
-
1239
- return Lookup(
1240
- records=queryset,
1241
- values=[i.get(field) for i in queryset.values()],
1242
- tuple_name=cls.__class__.__name__,
1243
- prefix="ln",
1244
- ).lookup(
1245
- return_field=(
1246
- get_name_field(registry=queryset.model, field=return_field)
1247
- if return_field is not None
1248
- else None
1249
- )
1250
- )
1251
-
1252
-
1253
1072
  def get_name_field(
1254
1073
  registry: type[Record] | QuerySet | Manager,
1255
1074
  *,
lamindb/models/run.py CHANGED
@@ -347,7 +347,7 @@ class ParamValue(Record):
347
347
 
348
348
 
349
349
  class Run(Record):
350
- """Runs.
350
+ """Runs of transforms such as the execution of a script.
351
351
 
352
352
  A registry to store runs of transforms, such as an executation of a script.
353
353
 
@@ -559,7 +559,7 @@ class Run(Record):
559
559
 
560
560
  Query by fields::
561
561
 
562
- ln.Run.filter(key="my_datasets/my_file.parquet")
562
+ ln.Run.filter(key="examples/my_file.parquet")
563
563
 
564
564
  Query by params::
565
565
 
@@ -614,9 +614,9 @@ def delete_run_artifacts(run: Run) -> None:
614
614
 
615
615
  class RunParamValue(BasicRecord, LinkORM):
616
616
  id: int = models.BigAutoField(primary_key=True)
617
- run: Run = ForeignKey(Run, CASCADE, related_name="+")
617
+ run: Run = ForeignKey(Run, CASCADE, related_name="links_paramvalue")
618
618
  # we follow the lower() case convention rather than snake case for link models
619
- paramvalue: ParamValue = ForeignKey(ParamValue, PROTECT, related_name="+")
619
+ paramvalue: ParamValue = ForeignKey(ParamValue, PROTECT, related_name="links_run")
620
620
  created_at: datetime = DateTimeField(
621
621
  editable=False, db_default=models.functions.Now(), db_index=True
622
622
  )
lamindb/models/save.py CHANGED
@@ -30,7 +30,7 @@ if TYPE_CHECKING:
30
30
 
31
31
 
32
32
  def save(records: Iterable[Record], ignore_conflicts: bool | None = False) -> None:
33
- """Bulk save to registries & storage.
33
+ """Bulk save records.
34
34
 
35
35
  Note:
36
36
 
@@ -157,7 +157,13 @@ def check_and_attempt_upload(
157
157
  return exception
158
158
  # copies (if on-disk) or moves the temporary file (if in-memory) to the cache
159
159
  if os.getenv("LAMINDB_MULTI_INSTANCE") is None:
160
- copy_or_move_to_cache(artifact, storage_path, cache_path)
160
+ # this happens only after the actual upload was performed
161
+ # we avoid failing here in case any problems happen in copy_or_move_to_cache
162
+ # because the cache copying or cleanup is not absolutely necessary
163
+ try:
164
+ copy_or_move_to_cache(artifact, storage_path, cache_path)
165
+ except Exception as e:
166
+ logger.warning(f"A problem with cache on saving: {e}")
161
167
  # after successful upload, we should remove the attribute so that another call
162
168
  # call to save won't upload again, the user should call replace() then
163
169
  del artifact._local_filepath