lamindb_setup 0.73.2__py2.py3-none-any.whl → 0.74.0__py2.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.
lamindb_setup/__init__.py CHANGED
@@ -34,7 +34,7 @@ Modules & settings:
34
34
 
35
35
  """
36
36
 
37
- __version__ = "0.73.2" # denote a release candidate for 0.1.0 with 0.1rc1
37
+ __version__ = "0.74.0" # denote a release candidate for 0.1.0 with 0.1rc1
38
38
 
39
39
  import sys
40
40
  from os import name as _os_name
@@ -33,8 +33,7 @@ def get_schema_module_name(schema_name) -> str:
33
33
  if module_spec is not None:
34
34
  return name
35
35
  raise ImportError(
36
- f"Python package for '{schema_name}' is not installed, tried two package names:"
37
- f" {name_attempts}\nHave you installed the schema package using `pip install`?"
36
+ f"Python package for '{schema_name}' is not installed.\nIf your package is on PyPI, run `pip install {schema_name}`"
38
37
  )
39
38
 
40
39
 
@@ -256,7 +255,10 @@ def init(
256
255
  schema=schema,
257
256
  uid=ssettings.uid,
258
257
  )
259
- if isettings.is_remote and instance_state != "instance-corrupted-or-deleted":
258
+ register_on_hub = (
259
+ isettings.is_remote and instance_state != "instance-corrupted-or-deleted"
260
+ )
261
+ if register_on_hub:
260
262
  init_instance_hub(isettings)
261
263
  validate_sqlite_state(isettings)
262
264
  isettings._persist()
@@ -270,8 +272,10 @@ def init(
270
272
  "locked instance (to unlock and push changes to the cloud SQLite file,"
271
273
  " call: lamin close)"
272
274
  )
273
- # we can debate whether this is the right setting, but this is how
274
- # things have been and we'd like to not easily break backward compat
275
+ if register_on_hub and isettings.dialect != "sqlite":
276
+ from ._schema_metadata import update_schema_in_hub
277
+
278
+ update_schema_in_hub()
275
279
  settings.auto_connect = True
276
280
  except Exception as e:
277
281
  from ._delete import delete_by_isettings
lamindb_setup/_migrate.py CHANGED
@@ -75,21 +75,15 @@ class migrate:
75
75
  from lamindb_setup.core._hub_client import call_with_fallback_auth
76
76
  from lamindb_setup.core._hub_crud import (
77
77
  select_collaborator,
78
- select_instance_by_id,
79
78
  update_instance,
80
79
  )
81
80
 
82
- instance_id_str = settings.instance._id.hex
83
- instance = call_with_fallback_auth(
84
- select_instance_by_id, instance_id=instance_id_str
85
- )
86
- instance_is_on_hub = instance is not None
87
- if instance_is_on_hub:
81
+ if settings.instance.is_on_hub:
88
82
  # double check that user is an admin, otherwise will fail below
89
- # without idempotence
83
+ # due to insufficient SQL permissions with cryptic error
90
84
  collaborator = call_with_fallback_auth(
91
85
  select_collaborator,
92
- instance_id=instance_id_str,
86
+ instance_id=settings.instance._id,
93
87
  account_id=settings.user._uuid,
94
88
  )
95
89
  if collaborator is None or collaborator["role"] != "admin":
@@ -104,7 +98,7 @@ class migrate:
104
98
  # this sets up django and deploys the migrations
105
99
  setup_django(settings.instance, deploy_migrations=True)
106
100
  # this populates the hub
107
- if instance_is_on_hub:
101
+ if settings.instance.is_on_hub:
108
102
  logger.important(f"updating lamindb version in hub: {lamindb.__version__}")
109
103
  # TODO: integrate update of instance table within update_schema_in_hub & below
110
104
  if settings.instance.dialect != "sqlite":
@@ -27,3 +27,7 @@ def register(_test: bool = False):
27
27
  init_instance_hub(isettings)
28
28
  isettings._is_on_hub = True
29
29
  isettings._persist()
30
+ if isettings.dialect != "sqlite" and not _test:
31
+ from ._schema_metadata import update_schema_in_hub
32
+
33
+ update_schema_in_hub()
@@ -3,31 +3,26 @@ from __future__ import annotations
3
3
  import hashlib
4
4
  import importlib
5
5
  import json
6
- from dataclasses import dataclass
7
- from typing import TYPE_CHECKING, Dict
6
+ from typing import TYPE_CHECKING, Literal
8
7
  from uuid import UUID
9
8
 
10
- import sqlparse
11
- from django.contrib.postgres.expressions import ArraySubquery
12
9
  from django.db.models import (
13
10
  Field,
11
+ ForeignKey,
14
12
  ForeignObjectRel,
15
13
  ManyToManyField,
16
14
  ManyToManyRel,
17
- OuterRef,
18
- QuerySet,
19
- Subquery,
15
+ ManyToOneRel,
16
+ OneToOneField,
17
+ OneToOneRel,
20
18
  )
21
- from django.db.models.functions import JSONObject
22
- from sqlparse.sql import Identifier, IdentifierList
23
- from sqlparse.tokens import DML, Keyword
19
+ from pydantic import BaseModel
24
20
 
25
21
  from lamindb_setup import settings
26
22
  from lamindb_setup._init_instance import get_schema_module_name
27
23
  from lamindb_setup.core._hub_client import call_with_fallback_auth
28
24
 
29
25
  if TYPE_CHECKING:
30
- from lnschema_core.models import Registry
31
26
  from supabase import Client
32
27
 
33
28
 
@@ -36,7 +31,7 @@ def update_schema_in_hub() -> tuple[bool, UUID, dict]:
36
31
 
37
32
 
38
33
  def _synchronize_schema(client: Client) -> tuple[bool, UUID, dict]:
39
- schema_metadata = SchemaMetadata()
34
+ schema_metadata = _SchemaHandler()
40
35
  schema_metadata_dict = schema_metadata.to_json()
41
36
  schema_uuid = _dict_to_uuid(schema_metadata_dict)
42
37
  schema = _get_schema_by_id(schema_uuid, client)
@@ -52,7 +47,7 @@ def _synchronize_schema(client: Client) -> tuple[bool, UUID, dict]:
52
47
  "id": schema_uuid.hex,
53
48
  "module_ids": module_ids,
54
49
  "module_set_info": module_set_info,
55
- "json": schema_metadata_dict,
50
+ "schema_json": schema_metadata_dict,
56
51
  }
57
52
  )
58
53
  .execute()
@@ -90,100 +85,47 @@ def _dict_to_uuid(dict: dict):
90
85
  return uuid
91
86
 
92
87
 
93
- class SchemaMetadata:
94
- def __init__(self) -> None:
95
- self.included_modules = ["core"] + list(settings.instance.schema)
96
- self.modules = self._get_modules_metadata()
97
-
98
- def to_dict(
99
- self, include_django_objects: bool = True, include_select_terms: bool = True
100
- ):
101
- return {
102
- module_name: {
103
- model_name: model.to_dict(include_django_objects, include_select_terms)
104
- for model_name, model in module.items()
105
- }
106
- for module_name, module in self.modules.items()
107
- }
108
-
109
- def to_json(self, include_select_terms: bool = True):
110
- return self.to_dict(
111
- include_django_objects=False, include_select_terms=include_select_terms
112
- )
113
-
114
- def _get_modules_metadata(self):
115
- return {
116
- module_name: {
117
- model._meta.model_name: ModelMetadata(
118
- model, module_name, self.included_modules
119
- )
120
- for model in self._get_schema_module(
121
- module_name
122
- ).models.__dict__.values()
123
- if model.__class__.__name__ == "ModelBase"
124
- and model.__name__ not in ["Registry", "ORM"]
125
- and not model._meta.abstract
126
- and model.__get_schema_name__() == module_name
127
- }
128
- for module_name in self.included_modules
129
- }
130
-
131
- def _get_module_set_info(self):
132
- # TODO: rely on schemamodule table for this
133
- module_set_info = []
134
- for module_name in self.included_modules:
135
- module = self._get_schema_module(module_name)
136
- module_set_info.append(
137
- {"id": 0, "name": module_name, "version": module.__version__}
138
- )
139
- return module_set_info
140
-
141
- @staticmethod
142
- def _get_schema_module(module_name):
143
- return importlib.import_module(get_schema_module_name(module_name))
144
-
145
-
146
- @dataclass
147
- class FieldMetadata:
148
- schema_name: str
149
- model_name: str
88
+ RelationType = Literal["many-to-one", "one-to-many", "many-to-many", "one-to-one"]
89
+ Type = Literal[
90
+ "ForeignKey",
91
+ "CharField",
92
+ "DateTimeField",
93
+ "AutoField",
94
+ "BooleanField",
95
+ "BigIntegerField",
96
+ "SmallIntegerField",
97
+ "TextField",
98
+ "BigAutoField",
99
+ "ManyToManyField",
100
+ "IntegerField",
101
+ "OneToOneField",
102
+ "JSONField",
103
+ "DateField",
104
+ "FloatField",
105
+ ]
106
+
107
+
108
+ class Through(BaseModel):
109
+ left_key: str
110
+ right_key: str
111
+ link_table_name: str | None = None
112
+
113
+
114
+ class FieldMetadata(BaseModel):
115
+ type: Type
116
+ column: str | None = None
117
+ through: Through | None = None
150
118
  field_name: str
151
- type: str
119
+ model_name: str
120
+ schema_name: str
152
121
  is_link_table: bool
153
- column: str | None = None
154
- relation_type: str | None = None
155
- related_schema_name: str | None = None
156
- related_model_name: str | None = None
122
+ relation_type: RelationType | None = None
157
123
  related_field_name: str | None = None
158
- through: dict | None = None
159
-
160
-
161
- class ModelRelations:
162
- def __init__(self, fields: list[ForeignObjectRel]) -> None:
163
- self.many_to_one = {}
164
- self.one_to_many = {}
165
- self.many_to_many = {}
166
- self.one_to_one = {}
167
-
168
- for field in fields:
169
- if field.many_to_one:
170
- self.many_to_one.update({field.name: field})
171
- elif field.one_to_many:
172
- self.one_to_many.update({field.name: field})
173
- elif field.many_to_many:
174
- self.many_to_many.update({field.name: field})
175
- elif field.one_to_one:
176
- self.one_to_one.update({field.name: field})
177
-
178
- self.all = {
179
- **self.many_to_one,
180
- **self.one_to_many,
181
- **self.many_to_many,
182
- **self.one_to_one,
183
- }
124
+ related_model_name: str | None = None
125
+ related_schema_name: str | None = None
184
126
 
185
127
 
186
- class ModelMetadata:
128
+ class _ModelHandler:
187
129
  def __init__(self, model, module_name: str, included_modules: list[str]) -> None:
188
130
  self.model = model
189
131
  self.class_name = model.__name__
@@ -191,39 +133,26 @@ class ModelMetadata:
191
133
  self.model_name = model._meta.model_name
192
134
  self.table_name = model._meta.db_table
193
135
  self.included_modules = included_modules
194
- self.fields, self.relations = self._get_fields_metadata(self.model)
136
+ self.fields = self._get_fields_metadata(self.model)
195
137
 
196
- def to_dict(
197
- self, include_django_objects: bool = True, include_select_terms: bool = True
198
- ):
138
+ def to_dict(self, include_django_objects: bool = True):
199
139
  _dict = {
200
140
  "fields": self.fields.copy(),
201
141
  "class_name": self.class_name,
202
142
  "table_name": self.table_name,
203
143
  }
204
144
 
205
- select_terms = self.select_terms if include_select_terms else []
206
-
207
145
  for field_name in self.fields.keys():
208
146
  _dict["fields"][field_name] = _dict["fields"][field_name].__dict__
209
- if field_name in select_terms:
210
- _dict["fields"][field_name].update(
211
- {"select_term": select_terms[field_name]}
212
- )
147
+ through = _dict["fields"][field_name]["through"]
148
+ if through is not None:
149
+ _dict["fields"][field_name]["through"] = through.__dict__
213
150
 
214
151
  if include_django_objects:
215
152
  _dict.update({"model": self.model})
216
153
 
217
154
  return _dict
218
155
 
219
- @property
220
- def select_terms(self):
221
- return (
222
- DjangoQueryBuilder(self.module_name, self.model_name)
223
- .add_all_sub_queries()
224
- .extract_select_terms()
225
- )
226
-
227
156
  def _get_fields_metadata(self, model):
228
157
  related_fields = []
229
158
  fields_metadata: dict[str, FieldMetadata] = {}
@@ -239,66 +168,37 @@ class ModelMetadata:
239
168
  ):
240
169
  related_fields.append(field)
241
170
 
242
- model_relations_metadata = ModelRelations(related_fields)
243
-
244
171
  related_fields_metadata = self._get_related_fields_metadata(
245
- model, model_relations_metadata
172
+ model, related_fields
246
173
  )
247
174
 
248
175
  fields_metadata = {**fields_metadata, **related_fields_metadata}
249
176
 
250
- return fields_metadata, model_relations_metadata
177
+ return fields_metadata
251
178
 
252
- def _get_related_fields_metadata(
253
- self, model, model_relations_metadata: ModelRelations
254
- ):
179
+ def _get_related_fields_metadata(self, model, fields: list[ForeignObjectRel]):
255
180
  related_fields: dict[str, FieldMetadata] = {}
256
181
 
257
- # Many to one (foreign key defined in the model)
258
- for link_field_name, link_field in model_relations_metadata.many_to_one.items():
259
- related_fields.update(
260
- {f"{link_field_name}": self._get_field_metadata(model, link_field)}
261
- )
262
- for field in link_field.related_model._meta.fields:
182
+ for field in fields:
183
+ if field.many_to_one:
263
184
  related_fields.update(
264
- {
265
- f"{link_field_name}__{field.name}": self._get_field_metadata(
266
- model, field
267
- )
268
- }
185
+ {f"{field.name}": self._get_field_metadata(model, field)}
269
186
  )
270
-
271
- # One to many (foreign key defined in the related model)
272
- for relation_name, relation in model_relations_metadata.one_to_many.items():
273
- # exclude self reference as it is already included in the many to one
274
- if relation.related_model == model:
275
- continue
276
- related_fields.update(
277
- {f"{relation_name}": self._get_field_metadata(model, relation.field)}
278
- )
279
-
280
- # One to one
281
- for link_field_name, link_field in model_relations_metadata.one_to_one.items():
282
- related_fields.update(
283
- {f"{link_field_name}": self._get_field_metadata(model, link_field)}
284
- )
285
- for field in link_field.related_model._meta.fields:
187
+ elif field.one_to_many:
188
+ # exclude self reference as it is already included in the many to one
189
+ if field.related_model == model:
190
+ continue
286
191
  related_fields.update(
287
- {
288
- f"{link_field_name}__{field.name}": self._get_field_metadata(
289
- model, field
290
- )
291
- }
192
+ {f"{field.name}": self._get_field_metadata(model, field.field)}
193
+ )
194
+ elif field.many_to_many:
195
+ related_fields.update(
196
+ {f"{field.name}": self._get_field_metadata(model, field)}
197
+ )
198
+ elif field.one_to_one:
199
+ related_fields.update(
200
+ {f"{field.name}": self._get_field_metadata(model, field)}
292
201
  )
293
-
294
- # Many to many
295
- for (
296
- link_field_name,
297
- link_field,
298
- ) in model_relations_metadata.many_to_many.items():
299
- related_fields.update(
300
- {f"{link_field_name}": self._get_field_metadata(model, link_field)}
301
- )
302
202
 
303
203
  return related_fields
304
204
 
@@ -332,45 +232,83 @@ class ModelMetadata:
332
232
  pass
333
233
 
334
234
  column = None
335
- if relation_type not in ["many-to-many", "one-to-one", "one-to-many"]:
336
- column = field.column
337
-
338
- through = None
339
- if relation_type == "many-to-many":
340
- through = self._get_through(model, field)
235
+ if relation_type not in ["many-to-many", "one-to-many"]:
236
+ if not isinstance(field, ForeignObjectRel):
237
+ column = field.column
238
+
239
+ if relation_type is None:
240
+ through = None
241
+ elif relation_type == "many-to-many":
242
+ through = self._get_through_many_to_many(field)
243
+ else:
244
+ through = self._get_through(field)
341
245
 
342
246
  return FieldMetadata(
343
- schema_name,
344
- model_name,
345
- field_name,
346
- internal_type,
347
- issubclass(field.model, LinkORM),
348
- column,
349
- relation_type,
350
- related_schema_name,
351
- related_model_name,
352
- related_field_name,
353
- through,
247
+ schema_name=schema_name,
248
+ model_name=model_name,
249
+ field_name=field_name,
250
+ type=internal_type,
251
+ is_link_table=issubclass(field.model, LinkORM),
252
+ column=column,
253
+ relation_type=relation_type,
254
+ related_schema_name=related_schema_name,
255
+ related_model_name=related_model_name,
256
+ related_field_name=related_field_name,
257
+ through=through,
354
258
  )
355
259
 
356
260
  @staticmethod
357
- def _get_through(model, field_or_rel: ManyToManyField | ManyToManyRel):
358
- table_name = model._meta.db_table
359
- related_table_name = field_or_rel.related_model._meta.db_table
261
+ def _get_through_many_to_many(field_or_rel: ManyToManyField | ManyToManyRel):
262
+ from lnschema_core.models import Registry
360
263
 
361
264
  if isinstance(field_or_rel, ManyToManyField):
362
- return {
363
- "link_table_name": field_or_rel.remote_field.through._meta.db_table,
364
- table_name: field_or_rel.m2m_column_name(),
365
- related_table_name: field_or_rel.m2m_reverse_name(),
366
- }
265
+ if field_or_rel.model != Registry:
266
+ return Through(
267
+ left_key=field_or_rel.m2m_column_name(),
268
+ right_key=field_or_rel.m2m_reverse_name(),
269
+ link_table_name=field_or_rel.remote_field.through._meta.db_table,
270
+ )
271
+ else:
272
+ return Through(
273
+ left_key=field_or_rel.m2m_reverse_name(),
274
+ right_key=field_or_rel.m2m_column_name(),
275
+ link_table_name=field_or_rel.remote_field.through._meta.db_table,
276
+ )
367
277
 
368
278
  if isinstance(field_or_rel, ManyToManyRel):
369
- return {
370
- "link_table_name": field_or_rel.through._meta.db_table,
371
- table_name: field_or_rel.field.m2m_column_name(),
372
- related_table_name: field_or_rel.field.m2m_reverse_name(),
373
- }
279
+ if field_or_rel.model != Registry:
280
+ return Through(
281
+ left_key=field_or_rel.field.m2m_reverse_name(),
282
+ right_key=field_or_rel.field.m2m_column_name(),
283
+ link_table_name=field_or_rel.through._meta.db_table,
284
+ )
285
+ else:
286
+ return Through(
287
+ left_key=field_or_rel.field.m2m_column_name(),
288
+ right_key=field_or_rel.field.m2m_reverse_name(),
289
+ link_table_name=field_or_rel.through._meta.db_table,
290
+ )
291
+
292
+ def _get_through(
293
+ self, field_or_rel: ForeignKey | OneToOneField | ManyToOneRel | OneToOneRel
294
+ ):
295
+ if isinstance(field_or_rel, ForeignObjectRel):
296
+ rel_1 = field_or_rel.field.related_fields[0][0]
297
+ rel_2 = field_or_rel.field.related_fields[0][1]
298
+ else:
299
+ rel_1 = field_or_rel.related_fields[0][0]
300
+ rel_2 = field_or_rel.related_fields[0][1]
301
+
302
+ if rel_1.model._meta.model_name == self.model._meta.model_name:
303
+ return Through(
304
+ left_key=rel_1.column,
305
+ right_key=rel_2.column,
306
+ )
307
+ else:
308
+ return Through(
309
+ left_key=rel_2.column,
310
+ right_key=rel_1.column,
311
+ )
374
312
 
375
313
  @staticmethod
376
314
  def _get_relation_type(model, field: Field):
@@ -391,89 +329,50 @@ class ModelMetadata:
391
329
  return None
392
330
 
393
331
 
394
- class DjangoQueryBuilder:
395
- def __init__(
396
- self, module_name: str, model_name: str, query_set: QuerySet | None = None
397
- ) -> None:
398
- self.schema_metadata = SchemaMetadata()
399
- self.module_name = module_name
400
- self.model_name = model_name
401
- self.model_metadata = self.schema_metadata.modules[module_name][model_name]
402
- self.query_set = query_set if query_set else self.model_metadata.model.objects
403
-
404
- def add_all_sub_queries(self):
405
- all_fields = self.model_metadata.fields
406
- included_relations = [
407
- field_name
408
- for field_name, field in all_fields.items()
409
- if field.relation_type is not None
410
- ]
411
- self.add_sub_queries(included_relations)
412
- return self
413
-
414
- def add_sub_queries(self, included_relations: list[str]):
415
- sub_queries = {
416
- f"annotated_{relation_name}": self._get_sub_query(
417
- self.model_metadata.fields[relation_name]
418
- )
419
- for relation_name in included_relations
420
- }
421
- self.query_set = self.query_set.annotate(**sub_queries)
422
- return self
423
-
424
- def extract_select_terms(self):
425
- parsed = sqlparse.parse(self.sql_query)
426
- select_found = False
427
- select_terms = {}
428
-
429
- def get_name(identifier):
430
- name = identifier.get_name()
431
- return name if name is not None else str(identifier).split(".")
432
-
433
- for token in parsed[0].tokens:
434
- if token.ttype is DML and token.value.upper() == "SELECT":
435
- select_found = True
436
- elif select_found and isinstance(token, IdentifierList):
437
- for identifier in token.get_identifiers():
438
- select_terms[get_name(identifier)] = str(identifier)
439
- elif select_found and isinstance(token, Identifier):
440
- select_terms[get_name(token)] = str(token)
441
- elif token.ttype is Keyword:
442
- if token.value.upper() in ["FROM", "WHERE", "GROUP BY", "ORDER BY"]:
443
- break
444
-
445
- return select_terms
446
-
447
- def _get_sub_query(self, field_metadata: FieldMetadata):
448
- module_name = field_metadata.related_schema_name
449
- model_name = field_metadata.related_model_name
450
- field_name = field_metadata.related_field_name
451
- model_metadata = self.schema_metadata.modules[module_name][model_name]
452
- query_set = model_metadata.model.objects.get_queryset()
453
- select = {
454
- field_name: field_name
455
- for field_name in model_metadata.fields.keys()
456
- if model_metadata.fields[field_name].relation_type is None
457
- and "__" not in field_name
332
+ class _SchemaHandler:
333
+ def __init__(self) -> None:
334
+ self.included_modules = ["core"] + list(settings.instance.schema)
335
+ self.modules = self._get_modules_metadata()
336
+
337
+ def to_dict(self, include_django_objects: bool = True):
338
+ return {
339
+ module_name: {
340
+ model_name: model.to_dict(include_django_objects)
341
+ for model_name, model in module.items()
342
+ }
343
+ for module_name, module in self.modules.items()
458
344
  }
459
345
 
460
- if field_metadata.relation_type in ["many-to-many", "one-to-many"]:
461
- return ArraySubquery(
462
- Subquery(
463
- query_set.filter(**{field_name: OuterRef("pk")}).values(
464
- data=JSONObject(**select)
465
- )[:5]
346
+ def to_json(self):
347
+ return self.to_dict(include_django_objects=False)
348
+
349
+ def _get_modules_metadata(self):
350
+ return {
351
+ module_name: {
352
+ model._meta.model_name: _ModelHandler(
353
+ model, module_name, self.included_modules
466
354
  )
355
+ for model in self._get_schema_module(
356
+ module_name
357
+ ).models.__dict__.values()
358
+ if model.__class__.__name__ == "RegistryMeta"
359
+ and model.__name__ not in ["Registry", "ORM"]
360
+ and not model._meta.abstract
361
+ and model.__get_schema_name__() == module_name
362
+ }
363
+ for module_name in self.included_modules
364
+ }
365
+
366
+ def _get_module_set_info(self):
367
+ # TODO: rely on schemamodule table for this
368
+ module_set_info = []
369
+ for module_name in self.included_modules:
370
+ module = self._get_schema_module(module_name)
371
+ module_set_info.append(
372
+ {"id": 0, "name": module_name, "version": module.__version__}
467
373
  )
468
- if field_metadata.relation_type in ["many-to-one", "one-to-one"]:
469
- return Subquery(
470
- query_set.filter(**{field_name: OuterRef("pk")}).values(
471
- data=JSONObject(**select)
472
- )[:5]
473
- )
374
+ return module_set_info
474
375
 
475
- @property
476
- def sql_query(self):
477
- sql_template, params = self.query_set.query.sql_with_params()
478
- sql_query = sql_template % tuple(f"'{p}'" for p in params)
479
- return sql_query.replace("annotated_", "")
376
+ @staticmethod
377
+ def _get_schema_module(module_name):
378
+ return importlib.import_module(get_schema_module_name(module_name))
@@ -119,8 +119,8 @@ def call_with_fallback_auth(
119
119
  for renew_token, fallback_env in [(False, False), (True, False), (False, True)]:
120
120
  try:
121
121
  if renew_token:
122
- logger.important(
123
- "Renewing expired lamin token: call lamin login to avoid this"
122
+ logger.warning(
123
+ "renewing expired lamin token: call `lamin login` to avoid this"
124
124
  )
125
125
  client = connect_hub_with_auth(
126
126
  renew_token=renew_token, fallback_env=fallback_env
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def find_vscode_stubs_folder() -> Path | None:
8
+ # Possible locations of VSCode extensions
9
+ possible_locations = [
10
+ Path.home() / ".vscode" / "extensions", # Linux and macOS
11
+ Path.home() / ".vscode-server" / "extensions", # Remote development
12
+ Path(os.environ.get("APPDATA", "")) / "Code" / "User" / "extensions", # Windows
13
+ Path("/usr/share/code/resources/app/extensions"), # Some Linux distributions
14
+ ]
15
+ for location in possible_locations:
16
+ if location.exists():
17
+ # Look for Pylance extension folder
18
+ pylance_folders = list(location.glob("ms-python.vscode-pylance-*"))
19
+ if pylance_folders:
20
+ # Sort to get the latest version
21
+ latest_pylance = sorted(pylance_folders)[-1]
22
+ stubs_folder = (
23
+ latest_pylance / "dist" / "bundled" / "stubs" / "django-stubs"
24
+ )
25
+ if stubs_folder.exists():
26
+ return stubs_folder
27
+
28
+ return None
29
+
30
+
31
+ def private_django_api(reverse=False):
32
+ from django import db
33
+
34
+ # the order here matters
35
+ # changing it might break the tests
36
+ attributes = [
37
+ "DoesNotExist",
38
+ "MultipleObjectsReturned",
39
+ "add_to_class",
40
+ "adelete",
41
+ "refresh_from_db",
42
+ "asave",
43
+ "clean",
44
+ "clean_fields",
45
+ "date_error_message",
46
+ "get_constraints",
47
+ "get_deferred_fields",
48
+ "prepare_database_save",
49
+ "save_base",
50
+ "serializable_value",
51
+ "unique_error_message",
52
+ "validate_constraints",
53
+ "validate_unique",
54
+ ]
55
+ if reverse:
56
+ attributes.append("arefresh_from_db")
57
+ attributes.append("full_clean")
58
+ else:
59
+ attributes.append("a_refresh_from_db")
60
+ attributes.append("full__clean")
61
+
62
+ django_path = Path(db.__file__).parent.parent
63
+
64
+ encoding = "utf8" if os.name == "nt" else None
65
+
66
+ def prune_file(file_path):
67
+ content = file_path.read_text(encoding=encoding)
68
+ original_content = content
69
+
70
+ for attr in attributes:
71
+ old_name = f"_{attr}" if reverse else attr
72
+ new_name = attr if reverse else f"_{attr}"
73
+ content = content.replace(old_name, new_name)
74
+
75
+ if not reverse:
76
+ content = content.replace("Field_DoesNotExist", "FieldDoesNotExist")
77
+ content = content.replace("Object_DoesNotExist", "ObjectDoesNotExist")
78
+
79
+ if content != original_content:
80
+ file_path.write_text(content, encoding=encoding)
81
+
82
+ for file_path in django_path.rglob("*.py"):
83
+ prune_file(file_path)
84
+
85
+ pylance_path = find_vscode_stubs_folder()
86
+ if pylance_path is not None:
87
+ for file_path in pylance_path.rglob("*.pyi"):
88
+ prune_file(file_path)
@@ -27,6 +27,7 @@ class SetupSettings:
27
27
  _instance_settings_env: str | None = None
28
28
 
29
29
  _auto_connect_path: Path = settings_dir / "auto_connect"
30
+ _private_django_api_path: Path = settings_dir / "private_django_api"
30
31
 
31
32
  @property
32
33
  def _instance_settings_path(self) -> Path:
@@ -34,11 +35,17 @@ class SetupSettings:
34
35
 
35
36
  @property
36
37
  def settings_dir(self) -> Path:
38
+ """The directory that holds locally persisted settings."""
37
39
  return settings_dir
38
40
 
39
41
  @property
40
42
  def auto_connect(self) -> bool:
41
- """Auto-connect to loaded instance upon lamindb import."""
43
+ """Auto-connect to loaded instance upon lamindb import.
44
+
45
+ `lamin init` and `lamin load` switch this to `True`.
46
+
47
+ `ln.connect()` doesn't change the value of this setting.
48
+ """
42
49
  return self._auto_connect_path.exists()
43
50
 
44
51
  @auto_connect.setter
@@ -48,10 +55,35 @@ class SetupSettings:
48
55
  else:
49
56
  self._auto_connect_path.unlink(missing_ok=True)
50
57
 
58
+ @property
59
+ def private_django_api(self) -> bool:
60
+ """Turn internal Django API private to clean up the API (default `False`).
61
+
62
+ This patches your local pip-installed django installation. You can undo
63
+ the patch by setting this back to `False`.
64
+ """
65
+ return self._private_django_api_path.exists()
66
+
67
+ @private_django_api.setter
68
+ def private_django_api(self, value: bool) -> None:
69
+ from ._private_django_api import private_django_api
70
+
71
+ # we don't want to call private_django_api() twice
72
+ if value and not self.private_django_api:
73
+ private_django_api()
74
+ self._private_django_api_path.touch()
75
+ elif not value and self.private_django_api:
76
+ private_django_api(reverse=True)
77
+ self._private_django_api_path.unlink(missing_ok=True)
78
+
51
79
  @property
52
80
  def user(self) -> UserSettings:
53
- """:class:`~lamindb.setup.core.UserSettings`."""
54
- if self._user_settings is None or self._user_settings_env != get_env_name():
81
+ """Settings of current user."""
82
+ env_changed = (
83
+ self._user_settings_env is not None
84
+ and self._user_settings_env != get_env_name()
85
+ )
86
+ if self._user_settings is None or env_changed:
55
87
  self._user_settings = load_or_create_user_settings()
56
88
  self._user_settings_env = get_env_name()
57
89
  if self._user_settings and self._user_settings.uid is None:
@@ -60,18 +92,19 @@ class SetupSettings:
60
92
 
61
93
  @property
62
94
  def instance(self) -> InstanceSettings:
63
- """:class:`~lamindb.setup.core.InstanceSettings`."""
64
- if (
65
- self._instance_settings is None
66
- or self._instance_settings_env != get_env_name()
67
- ):
95
+ """Settings of current LaminDB instance."""
96
+ env_changed = (
97
+ self._instance_settings_env is not None
98
+ and self._instance_settings_env != get_env_name()
99
+ )
100
+ if self._instance_settings is None or env_changed:
68
101
  self._instance_settings = load_instance_settings()
69
102
  self._instance_settings_env = get_env_name()
70
103
  return self._instance_settings # type: ignore
71
104
 
72
105
  @property
73
106
  def storage(self) -> StorageSettings:
74
- """:class:`~lamindb.setup.core.StorageSettings`."""
107
+ """Settings of default storage."""
75
108
  return self.instance.storage
76
109
 
77
110
  @property
@@ -87,6 +120,7 @@ class SetupSettings:
87
120
  """Rich string representation."""
88
121
  repr = self.user.__repr__()
89
122
  repr += f"\nAuto-connect in Python: {self.auto_connect}\n"
123
+ repr += f"Private Django API: {self.private_django_api}\n"
90
124
  if self._instance_exists:
91
125
  repr += self.instance.__repr__()
92
126
  else:
@@ -106,7 +106,8 @@ class InstanceSettings:
106
106
  if local_root is not None:
107
107
  local_records = Storage.objects.filter(root=local_root)
108
108
  else:
109
- local_records = Storage.objects.filter(type="local")
109
+ # only search local managed storage locations (instance_uid=self.uid)
110
+ local_records = Storage.objects.filter(type="local", instance_uid=self.uid)
110
111
  all_local_records = local_records.all()
111
112
  try:
112
113
  # trigger an error in case of a migration issue
@@ -121,6 +121,8 @@ def init_storage(
121
121
  init_storage_hub(ssettings)
122
122
  # below comes last only if everything else was successful
123
123
  try:
124
+ # (federated) credentials for AWS access are provisioned under-the-hood
125
+ # discussion: https://laminlabs.slack.com/archives/C04FPE8V01W/p1719260587167489
124
126
  mark_storage_root(ssettings.root, ssettings.uid) # type: ignore
125
127
  except Exception:
126
128
  logger.important(
@@ -12,7 +12,10 @@ from __future__ import annotations
12
12
 
13
13
  import base64
14
14
  import hashlib
15
- from typing import TYPE_CHECKING
15
+ from concurrent.futures import ThreadPoolExecutor
16
+ from typing import TYPE_CHECKING, Iterable
17
+
18
+ import psutil
16
19
 
17
20
  if TYPE_CHECKING:
18
21
  from .types import Path, UPathStr
@@ -40,7 +43,7 @@ def hash_set(s: set[str]) -> str:
40
43
  return to_b64_str(hashlib.md5(bstr).digest())[:20]
41
44
 
42
45
 
43
- def hash_md5s_from_dir(hashes: list[str]) -> tuple[str, str]:
46
+ def hash_md5s_from_dir(hashes: Iterable[str]) -> tuple[str, str]:
44
47
  # need to sort below because we don't want the order of parsing the dir to
45
48
  # affect the hash
46
49
  digests = b"".join(
@@ -83,3 +86,27 @@ def hash_file(
83
86
  ).digest()
84
87
  hash_type = "sha1-fl"
85
88
  return to_b64_str(digest)[:22], hash_type
89
+
90
+
91
+ def hash_dir(path: Path):
92
+ files = (subpath for subpath in path.rglob("*") if subpath.is_file())
93
+
94
+ def hash_size(file):
95
+ file_size = file.stat().st_size
96
+ return hash_file(file, file_size)[0], file_size
97
+
98
+ try:
99
+ n_workers = len(psutil.Process().cpu_affinity())
100
+ except AttributeError:
101
+ n_workers = psutil.cpu_count()
102
+ if n_workers > 1:
103
+ with ThreadPoolExecutor(n_workers) as pool:
104
+ hashes_sizes = pool.map(hash_size, files)
105
+ else:
106
+ hashes_sizes = map(hash_size, files)
107
+ hashes, sizes = zip(*hashes_sizes)
108
+
109
+ hash, hash_type = hash_md5s_from_dir(hashes)
110
+ n_objects = len(hashes)
111
+ size = sum(sizes)
112
+ return size, hash, hash_type, n_objects
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lamindb_setup
3
- Version: 0.73.2
3
+ Version: 0.74.0
4
4
  Summary: Setup & configure LaminDB.
5
5
  Author-email: Lamin Labs <laminlabs@gmail.com>
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: lnschema_core>=0.51.0
8
8
  Requires-Dist: lamin_utils>=0.3.3
9
- Requires-Dist: django>4.2,<5.2.0
9
+ Requires-Dist: django>4.2,<5.3.0
10
10
  Requires-Dist: dj_database_url>=1.3.0,<3.0.0
11
11
  Requires-Dist: pydantic[dotenv]<2.0.0
12
12
  Requires-Dist: appdirs<2.0.0
@@ -1,4 +1,4 @@
1
- lamindb_setup/__init__.py,sha256=HyUA-3vUuT5XnO6MHe78x0OQaPP0s4-jb6QuFXEuY3s,1542
1
+ lamindb_setup/__init__.py,sha256=cJQ2NUW0T7g_vpNTd21VpzvTGNT733H0Kb39cuMH0Sk,1542
2
2
  lamindb_setup/_cache.py,sha256=wA7mbysANwe8hPNbjDo9bOmXJ0xIyaS5iyxIpxSWji4,846
3
3
  lamindb_setup/_check.py,sha256=28PcG8Kp6OpjSLSi1r2boL2Ryeh6xkaCL87HFbjs6GA,129
4
4
  lamindb_setup/_check_setup.py,sha256=cNEL9Q4yPpmEkGKHH8JgullWl1VUZwALJ4RHn9wZypY,2613
@@ -8,11 +8,11 @@ lamindb_setup/_delete.py,sha256=Y8KSFYgY0UHAvjd7cCL6hZ_XiLeJwx50BguVATcj_Xo,5524
8
8
  lamindb_setup/_django.py,sha256=EoyWvFzH0i9wxjy4JZhcoXCTckztP_Mrl6FbYQnMmLE,1534
9
9
  lamindb_setup/_exportdb.py,sha256=uTIZjKKTB7arzEr1j0O6lONiT2pRBKeOFdLvOV8ZwzE,2120
10
10
  lamindb_setup/_importdb.py,sha256=yYYShzUajTsR-cTW4CZ-UNDWZY2uE5PAgNbp-wn8Ogc,1874
11
- lamindb_setup/_init_instance.py,sha256=Cji3h2kCj8Meukkm8Btl8Lu4Jio4NiWLlzAAKukEzYY,11896
12
- lamindb_setup/_migrate.py,sha256=49xPkwU-QQjpq0xSUepiVfvRsMhmjfbRlhd72YIq8o8,9059
13
- lamindb_setup/_register_instance.py,sha256=Jeu0wyvJVSVQ_n-A_7yn7xOZIP0ncJD92DRABqzPIjA,940
11
+ lamindb_setup/_init_instance.py,sha256=e6OJSpN2eJZd22kAfUVEUbqXF48FzNfLc5xMTS8gZ84,11919
12
+ lamindb_setup/_migrate.py,sha256=P4n3x0SYzO9szjF2-JMa7z4mQadtWjHv5ow4HbCDZLI,8864
13
+ lamindb_setup/_register_instance.py,sha256=LlD2n44Gmh8OXrAkL9iL9NiML8QDKnk4d869MhXoOWI,1083
14
14
  lamindb_setup/_schema.py,sha256=b3uzhhWpV5mQtDwhMINc2MabGCnGLESy51ito3yl6Wc,679
15
- lamindb_setup/_schema_metadata.py,sha256=G3yXJ46OkVGqHxccCgjvQnnBNKg1uhwxXB1CddlkwYw,16882
15
+ lamindb_setup/_schema_metadata.py,sha256=QL7Ord0BnKQnqZpLAjQX2gLhLwF_9zmZFcH8EjEMrfA,12895
16
16
  lamindb_setup/_set_managed_storage.py,sha256=mNZrANn-9rwZ0oGWxxg0wS0T0VOQCWyo4nSSyNAE15Q,1419
17
17
  lamindb_setup/_setup_user.py,sha256=6Oc7Rke-yRQSZbuntdUAz8QbJ6UuPzYHI9FnYlf_q-A,3670
18
18
  lamindb_setup/_silence_loggers.py,sha256=AKF_YcHvX32eGXdsYK8MJlxEaZ-Uo2f6QDRzjKFCtws,1568
@@ -21,25 +21,26 @@ lamindb_setup/core/_aws_credentials.py,sha256=uKMQO9q42Hnepz8aj3RxwLKDWUJx8pNOYr
21
21
  lamindb_setup/core/_aws_storage.py,sha256=nEjeUv4xUVpoV0Lx-zjjmyb9w804bDyaeiM-OqbfwM0,1799
22
22
  lamindb_setup/core/_deprecated.py,sha256=3qxUI1dnDlSeR0BYrv7ucjqRBEojbqotPgpShXs4KF8,2520
23
23
  lamindb_setup/core/_docs.py,sha256=3k-YY-oVaJd_9UIY-LfBg_u8raKOCNfkZQPA73KsUhs,276
24
- lamindb_setup/core/_hub_client.py,sha256=V0qKDsCdRn-tQy2YIk4VgXcpJFmuum6N3gcavAC7gBQ,5504
24
+ lamindb_setup/core/_hub_client.py,sha256=g0Lmpv19xiUWoD67SpdT6re1EDfrd7D6Bsz4gIKpx_E,5504
25
25
  lamindb_setup/core/_hub_core.py,sha256=RGjTqf1owuWmkXAYy0EPaoHAaJ-0T0hAidkqa3cIdiM,16352
26
26
  lamindb_setup/core/_hub_crud.py,sha256=b1XF7AJpM9Q-ttm9nPG-r3OTRWHQaGzAGIyvmb83NTo,4859
27
27
  lamindb_setup/core/_hub_utils.py,sha256=b_M1LkdCjiMWm1EOlSb9GuPdLijwVgQDtATTpeZuXI0,1875
28
- lamindb_setup/core/_settings.py,sha256=jjZ_AxRXB3Y3UP6m04BAw_dhFbJbdg2-nZWmEv2LNZ8,3141
29
- lamindb_setup/core/_settings_instance.py,sha256=MJ1UArq49A168uGx9nOWDiXEKeSlDqtV-dqQf1kBcZw,17518
28
+ lamindb_setup/core/_private_django_api.py,sha256=KIn43HOhiRjkbTbddyJqv-WNTTa1bAizbM1tWXoXPBg,2869
29
+ lamindb_setup/core/_settings.py,sha256=46axQ5HPvI0X9YuotgdpuSOfSo7qYU1DudIx3vxpFk0,4471
30
+ lamindb_setup/core/_settings_instance.py,sha256=OMN6ZROF7iV9Juh0bgE9fOmedE7EJI_YniztwJ5-acI,17623
30
31
  lamindb_setup/core/_settings_load.py,sha256=NGgCDpN85j1EqoKlrYFIlZBMlBJm33gx2-wc96CP_ZQ,3922
31
32
  lamindb_setup/core/_settings_save.py,sha256=d1A-Ex-7H08mb8l7I0Oe0j0GilrfaDuprh_NMxhQAsQ,2704
32
- lamindb_setup/core/_settings_storage.py,sha256=k4XyJR6_KpUpQuBYZp4mEdABiT91gTTfbK7tAVwqZCA,13093
33
+ lamindb_setup/core/_settings_storage.py,sha256=65aobewYX6VfOeYZjZQOOI7ZD_3b4QA9TDmrduU0m4c,13262
33
34
  lamindb_setup/core/_settings_store.py,sha256=dagS5c7wAMRnuZTRfCU4sKaIOyF_HwAP5Fnnn8vphno,2084
34
35
  lamindb_setup/core/_settings_user.py,sha256=P2lC4WDRAFfT-Xq3MlXJ-wMKIHCoGNhMTQfRGIAyUNQ,1344
35
36
  lamindb_setup/core/_setup_bionty_sources.py,sha256=h_pBANsSGK6ujAFsG21mtADHVJoMLKDR4eGgRP4Fgls,3072
36
37
  lamindb_setup/core/cloud_sqlite_locker.py,sha256=reu02M4aE2BT_A5AFqwhv48l91mOMyQ4zTd-hh-wtuU,6922
37
38
  lamindb_setup/core/django.py,sha256=QUQm3zt5QIiD8uv6o9vbSm_bshqiSWzKSkgD3z2eJCg,3542
38
39
  lamindb_setup/core/exceptions.py,sha256=eoI7AXgATgDVzgArtN7CUvpaMUC067vsBg5LHCsWzDM,305
39
- lamindb_setup/core/hashing.py,sha256=7r96h5JBzuwfOR_gNNqTyWNPKMuiOUfBYwn6sCbZkf8,2269
40
+ lamindb_setup/core/hashing.py,sha256=_JliYeCcjT_foOUJ5ck1rvcCo9q7r4b4SaSclQ_w4Qo,3071
40
41
  lamindb_setup/core/types.py,sha256=bcYnZ0uM_2NXKJCl94Mmc-uYrQlRUUVKG3sK2N-F-N4,532
41
42
  lamindb_setup/core/upath.py,sha256=dwudkTVsXuyjS-2xR16WomcWtXJAEfRZ0ZzFq8_EDhE,27157
42
- lamindb_setup-0.73.2.dist-info/LICENSE,sha256=UOZ1F5fFDe3XXvG4oNnkL1-Ecun7zpHzRxjp-XsMeAo,11324
43
- lamindb_setup-0.73.2.dist-info/WHEEL,sha256=Sgu64hAMa6g5FdzHxXv9Xdse9yxpGGMeagVtPMWpJQY,99
44
- lamindb_setup-0.73.2.dist-info/METADATA,sha256=t3AiEbJJFkA0l-DY3DIDoLQHjBW--ToZWYS8vYNxdDU,1620
45
- lamindb_setup-0.73.2.dist-info/RECORD,,
43
+ lamindb_setup-0.74.0.dist-info/LICENSE,sha256=UOZ1F5fFDe3XXvG4oNnkL1-Ecun7zpHzRxjp-XsMeAo,11324
44
+ lamindb_setup-0.74.0.dist-info/WHEEL,sha256=Sgu64hAMa6g5FdzHxXv9Xdse9yxpGGMeagVtPMWpJQY,99
45
+ lamindb_setup-0.74.0.dist-info/METADATA,sha256=JdkBfYklKlMuRjUc1VGMGFYWM3E-v0alqmYaXI6Q2kM,1620
46
+ lamindb_setup-0.74.0.dist-info/RECORD,,