lamindb_setup 1.19.0__py3-none-any.whl → 1.19.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. lamindb_setup/__init__.py +1 -1
  2. lamindb_setup/_cache.py +87 -87
  3. lamindb_setup/_check.py +7 -7
  4. lamindb_setup/_check_setup.py +131 -131
  5. lamindb_setup/_connect_instance.py +443 -441
  6. lamindb_setup/_delete.py +155 -155
  7. lamindb_setup/_disconnect.py +38 -38
  8. lamindb_setup/_django.py +39 -39
  9. lamindb_setup/_entry_points.py +19 -19
  10. lamindb_setup/_init_instance.py +423 -423
  11. lamindb_setup/_migrate.py +331 -331
  12. lamindb_setup/_register_instance.py +32 -32
  13. lamindb_setup/_schema.py +27 -27
  14. lamindb_setup/_schema_metadata.py +451 -451
  15. lamindb_setup/_set_managed_storage.py +81 -81
  16. lamindb_setup/_setup_user.py +198 -198
  17. lamindb_setup/_silence_loggers.py +46 -46
  18. lamindb_setup/core/__init__.py +25 -34
  19. lamindb_setup/core/_aws_options.py +276 -276
  20. lamindb_setup/core/_aws_storage.py +57 -57
  21. lamindb_setup/core/_clone.py +50 -50
  22. lamindb_setup/core/_deprecated.py +62 -62
  23. lamindb_setup/core/_docs.py +14 -14
  24. lamindb_setup/core/_hub_client.py +288 -288
  25. lamindb_setup/core/_hub_crud.py +247 -247
  26. lamindb_setup/core/_hub_utils.py +100 -100
  27. lamindb_setup/core/_private_django_api.py +80 -80
  28. lamindb_setup/core/_settings.py +440 -434
  29. lamindb_setup/core/_settings_instance.py +22 -1
  30. lamindb_setup/core/_settings_load.py +162 -162
  31. lamindb_setup/core/_settings_save.py +108 -108
  32. lamindb_setup/core/_settings_storage.py +433 -433
  33. lamindb_setup/core/_settings_store.py +162 -162
  34. lamindb_setup/core/_settings_user.py +55 -55
  35. lamindb_setup/core/_setup_bionty_sources.py +44 -44
  36. lamindb_setup/core/cloud_sqlite_locker.py +240 -240
  37. lamindb_setup/core/django.py +414 -413
  38. lamindb_setup/core/exceptions.py +1 -1
  39. lamindb_setup/core/hashing.py +134 -134
  40. lamindb_setup/core/types.py +1 -1
  41. lamindb_setup/core/upath.py +1031 -1028
  42. lamindb_setup/errors.py +72 -72
  43. lamindb_setup/io.py +423 -423
  44. lamindb_setup/types.py +17 -17
  45. {lamindb_setup-1.19.0.dist-info → lamindb_setup-1.19.1.dist-info}/METADATA +3 -2
  46. lamindb_setup-1.19.1.dist-info/RECORD +51 -0
  47. {lamindb_setup-1.19.0.dist-info → lamindb_setup-1.19.1.dist-info}/WHEEL +1 -1
  48. {lamindb_setup-1.19.0.dist-info → lamindb_setup-1.19.1.dist-info/licenses}/LICENSE +201 -201
  49. lamindb_setup-1.19.0.dist-info/RECORD +0 -51
@@ -1,451 +1,451 @@
1
- from __future__ import annotations
2
-
3
- import hashlib
4
- import importlib
5
- import json
6
- from typing import TYPE_CHECKING, Literal
7
- from uuid import UUID
8
-
9
- from lamin_utils import logger
10
- from pydantic import BaseModel
11
-
12
- from lamindb_setup import settings
13
- from lamindb_setup._init_instance import get_schema_module_name
14
- from lamindb_setup.core._hub_client import call_with_fallback_auth
15
-
16
- # surpress pydantic warning about `model_` namespace
17
- try:
18
- BaseModel.model_config["protected_namespaces"] = ()
19
- logger.debug(
20
- "pydantic.BaseModel.model_config['protected_namespaces'] has been set to ()"
21
- )
22
- except Exception:
23
- pass
24
-
25
-
26
- if TYPE_CHECKING:
27
- from django.db.models import (
28
- Field,
29
- ForeignKey,
30
- ForeignObjectRel,
31
- ManyToManyField,
32
- ManyToManyRel,
33
- ManyToOneRel,
34
- OneToOneField,
35
- OneToOneRel,
36
- )
37
- from supabase import Client
38
-
39
-
40
- def update_schema_in_hub(access_token: str | None = None) -> tuple[bool, UUID, dict]:
41
- return call_with_fallback_auth(_synchronize_schema, access_token=access_token)
42
-
43
-
44
- def _synchronize_schema(client: Client) -> tuple[bool, UUID, dict]:
45
- schema_metadata = _SchemaHandler()
46
- schema_metadata_dict = schema_metadata.to_json()
47
- schema_uuid = _dict_to_uuid(schema_metadata_dict)
48
- schema = _get_schema_by_id(schema_uuid, client)
49
-
50
- is_new = schema is None
51
- if is_new:
52
- module_set_info = schema_metadata._get_module_set_info()
53
- module_ids = "-".join(str(module_info["id"]) for module_info in module_set_info)
54
- schema = (
55
- client.table("schema")
56
- .insert(
57
- {
58
- "id": schema_uuid.hex,
59
- "module_ids": module_ids,
60
- "module_set_info": module_set_info,
61
- "schema_json": schema_metadata_dict,
62
- }
63
- )
64
- .execute()
65
- .data[0]
66
- )
67
-
68
- instance_response = (
69
- client.table("instance")
70
- .update({"schema_id": schema_uuid.hex})
71
- .eq("id", settings.instance._id.hex)
72
- .execute()
73
- )
74
- assert len(instance_response.data) == 1, (
75
- f"schema of instance {settings.instance._id.hex} could not be updated with schema {schema_uuid.hex}"
76
- )
77
-
78
- return is_new, schema_uuid, schema
79
-
80
-
81
- def get_schema_by_id(id: UUID):
82
- return call_with_fallback_auth(_get_schema_by_id, id=id)
83
-
84
-
85
- def _get_schema_by_id(id: UUID, client: Client):
86
- response = client.table("schema").select("*").eq("id", id.hex).execute()
87
- if len(response.data) == 0:
88
- return None
89
- return response.data[0]
90
-
91
-
92
- def _dict_to_uuid(dict: dict):
93
- encoded = json.dumps(dict, sort_keys=True).encode("utf-8")
94
- hash = hashlib.md5(encoded).digest()
95
- uuid = UUID(bytes=hash[:16])
96
- return uuid
97
-
98
-
99
- RelationType = Literal["many-to-one", "one-to-many", "many-to-many", "one-to-one"]
100
- Type = Literal[
101
- "ForeignKey",
102
- # the following are generated with `from django.db import models; [attr for attr in dir(models) if attr.endswith('Field')]`
103
- "AutoField",
104
- "BigAutoField",
105
- "BigIntegerField",
106
- "BinaryField",
107
- "BooleanField",
108
- "CharField",
109
- "CommaSeparatedIntegerField",
110
- "DateField",
111
- "DateTimeField",
112
- "DecimalField",
113
- "DurationField",
114
- "EmailField",
115
- "Field",
116
- "FileField",
117
- "FilePathField",
118
- "FloatField",
119
- "GeneratedField",
120
- "GenericIPAddressField",
121
- "IPAddressField",
122
- "ImageField",
123
- "IntegerField",
124
- "JSONField",
125
- "ManyToManyField",
126
- "NullBooleanField",
127
- "OneToOneField",
128
- "PositiveBigIntegerField",
129
- "PositiveIntegerField",
130
- "PositiveSmallIntegerField",
131
- "SlugField",
132
- "SmallAutoField",
133
- "SmallIntegerField",
134
- "TextField",
135
- "TimeField",
136
- "URLField",
137
- "UUIDField",
138
- ]
139
-
140
-
141
- class Through(BaseModel):
142
- left_key: str
143
- right_key: str
144
- link_table_name: str | None = None
145
-
146
-
147
- class FieldMetadata(BaseModel):
148
- type: Type
149
- column_name: str | None = None
150
- through: Through | None = None
151
- field_name: str
152
- model_name: str
153
- schema_name: str
154
- is_link_table: bool
155
- is_primary_key: bool
156
- is_editable: bool
157
- max_length: int | None = None
158
- relation_type: RelationType | None = None
159
- related_field_name: str | None = None
160
- related_model_name: str | None = None
161
- related_schema_name: str | None = None
162
-
163
-
164
- class _ModelHandler:
165
- def __init__(self, model, module_name: str, included_modules: list[str]) -> None:
166
- from lamindb.models import IsLink
167
-
168
- self.model = model
169
- self.class_name = model.__name__
170
- self.module_name = module_name
171
- self.model_name = model._meta.model_name
172
- self.table_name = model._meta.db_table
173
- self.included_modules = included_modules
174
- self.fields = self._get_fields_metadata(self.model)
175
- self.is_auto_created = bool(model._meta.auto_created)
176
- self.is_link_table = issubclass(model, IsLink) or self.is_auto_created
177
- self.name_field = model._name_field if hasattr(model, "_name_field") else None
178
- self.ontology_id_field = (
179
- model._ontology_id_field if hasattr(model, "_ontology_id_field") else None
180
- )
181
-
182
- def to_dict(self, include_django_objects: bool = True):
183
- _dict = {
184
- "fields": self.fields.copy(),
185
- "class_name": self.class_name,
186
- "table_name": self.table_name,
187
- "is_auto_created": self.is_auto_created,
188
- "is_link_table": self.is_link_table,
189
- "name_field": self.name_field,
190
- "ontology_id_field": self.ontology_id_field,
191
- }
192
-
193
- for field_name in self.fields.keys():
194
- _dict["fields"][field_name] = _dict["fields"][field_name].__dict__
195
- through = _dict["fields"][field_name]["through"]
196
- if through is not None:
197
- _dict["fields"][field_name]["through"] = through.__dict__
198
-
199
- if include_django_objects:
200
- _dict.update({"model": self.model})
201
-
202
- return _dict
203
-
204
- def _get_fields_metadata(self, model):
205
- related_fields = []
206
- fields_metadata: dict[str, FieldMetadata] = {}
207
-
208
- for field in model._meta.get_fields():
209
- field_metadata = self._get_field_metadata(model, field)
210
- if field_metadata.related_schema_name is None:
211
- fields_metadata.update({field.name: field_metadata})
212
-
213
- if (
214
- field_metadata.related_schema_name in self.included_modules
215
- and field_metadata.schema_name in self.included_modules
216
- ):
217
- related_fields.append(field)
218
-
219
- related_fields_metadata = self._get_related_fields_metadata(
220
- model, related_fields
221
- )
222
-
223
- fields_metadata = {**fields_metadata, **related_fields_metadata}
224
-
225
- return fields_metadata
226
-
227
- def _get_related_fields_metadata(self, model, fields: list[ForeignObjectRel]):
228
- related_fields: dict[str, FieldMetadata] = {}
229
-
230
- for field in fields:
231
- if field.many_to_one:
232
- related_fields.update(
233
- {f"{field.name}": self._get_field_metadata(model, field)}
234
- )
235
- elif field.one_to_many:
236
- # exclude self reference as it is already included in the many to one
237
- if field.related_model == model:
238
- continue
239
- related_fields.update(
240
- {f"{field.name}": self._get_field_metadata(model, field.field)}
241
- )
242
- elif field.many_to_many:
243
- related_fields.update(
244
- {f"{field.name}": self._get_field_metadata(model, field)}
245
- )
246
- elif field.one_to_one:
247
- related_fields.update(
248
- {f"{field.name}": self._get_field_metadata(model, field)}
249
- )
250
-
251
- return related_fields
252
-
253
- def _get_field_metadata(self, model, field: Field):
254
- from lamindb.models import IsLink, Registry
255
-
256
- internal_type = field.get_internal_type()
257
- model_name = field.model._meta.model_name
258
- relation_type = self._get_relation_type(model, field)
259
-
260
- schema_name = Registry.__get_module_name__(field.model)
261
-
262
- if field.related_model is None:
263
- related_model_name = None
264
- related_schema_name = None
265
- related_field_name = None
266
- is_editable = field.editable
267
- max_length = field.max_length
268
- else:
269
- related_model_name = field.related_model._meta.model_name
270
- related_schema_name = Registry.__get_module_name__(field.related_model)
271
- related_field_name = field.remote_field.name
272
- is_editable = False
273
- max_length = None
274
-
275
- field_name = field.name
276
- is_primary_key = getattr(field, "primary_key", False)
277
-
278
- if relation_type in ["one-to-many"]:
279
- # For a one-to-many relation, the field belong
280
- # to the other model as a foreign key.
281
- # To make usage similar to other relation types
282
- # we need to invert model and related model.
283
- schema_name, related_schema_name = related_schema_name, schema_name
284
- model_name, related_model_name = related_model_name, model_name
285
- field_name, related_field_name = related_field_name, field_name
286
- pass
287
-
288
- column = None
289
- if relation_type not in ["many-to-many", "one-to-many"]:
290
- # have to reload it here in case reset happened
291
- from django.db.models import ForeignObjectRel
292
-
293
- if not isinstance(field, ForeignObjectRel):
294
- column = field.column
295
-
296
- if relation_type is None:
297
- through = None
298
- elif relation_type == "many-to-many":
299
- through = self._get_through_many_to_many(field)
300
- else:
301
- through = self._get_through(field)
302
-
303
- return FieldMetadata(
304
- schema_name=schema_name if schema_name != "lamindb" else "core",
305
- model_name=model_name,
306
- field_name=field_name,
307
- type=internal_type,
308
- is_link_table=issubclass(field.model, IsLink),
309
- is_primary_key=is_primary_key,
310
- is_editable=is_editable,
311
- max_length=max_length,
312
- column_name=column,
313
- relation_type=relation_type,
314
- related_schema_name=related_schema_name
315
- if related_schema_name != "lamindb"
316
- else "core",
317
- related_model_name=related_model_name,
318
- related_field_name=related_field_name,
319
- through=through,
320
- )
321
-
322
- @staticmethod
323
- def _get_through_many_to_many(field_or_rel: ManyToManyField | ManyToManyRel):
324
- # have to reload it here in case reset happened
325
- from django.db.models import ManyToManyField, ManyToManyRel
326
- from lamindb.models import Registry
327
-
328
- if isinstance(field_or_rel, ManyToManyField):
329
- if field_or_rel.model != Registry:
330
- return Through(
331
- left_key=field_or_rel.m2m_column_name(),
332
- right_key=field_or_rel.m2m_reverse_name(),
333
- link_table_name=field_or_rel.remote_field.through._meta.db_table,
334
- )
335
- else:
336
- return Through(
337
- left_key=field_or_rel.m2m_reverse_name(),
338
- right_key=field_or_rel.m2m_column_name(),
339
- link_table_name=field_or_rel.remote_field.through._meta.db_table,
340
- )
341
-
342
- if isinstance(field_or_rel, ManyToManyRel):
343
- if field_or_rel.model != Registry:
344
- return Through(
345
- left_key=field_or_rel.field.m2m_reverse_name(),
346
- right_key=field_or_rel.field.m2m_column_name(),
347
- link_table_name=field_or_rel.through._meta.db_table,
348
- )
349
- else:
350
- return Through(
351
- left_key=field_or_rel.field.m2m_column_name(),
352
- right_key=field_or_rel.field.m2m_reverse_name(),
353
- link_table_name=field_or_rel.through._meta.db_table,
354
- )
355
-
356
- def _get_through(
357
- self, field_or_rel: ForeignKey | OneToOneField | ManyToOneRel | OneToOneRel
358
- ):
359
- # have to reload it here in case reset happened
360
- from django.db.models import ForeignObjectRel
361
-
362
- if isinstance(field_or_rel, ForeignObjectRel):
363
- rel_1 = field_or_rel.field.related_fields[0][0]
364
- rel_2 = field_or_rel.field.related_fields[0][1]
365
- else:
366
- rel_1 = field_or_rel.related_fields[0][0]
367
- rel_2 = field_or_rel.related_fields[0][1]
368
-
369
- if rel_1.model._meta.model_name == self.model._meta.model_name:
370
- return Through(
371
- left_key=rel_1.column,
372
- right_key=rel_2.column,
373
- )
374
- else:
375
- return Through(
376
- left_key=rel_2.column,
377
- right_key=rel_1.column,
378
- )
379
-
380
- @staticmethod
381
- def _get_relation_type(model, field: Field):
382
- if field.many_to_one:
383
- # defined in the model
384
- if model == field.model:
385
- return "many-to-one"
386
- # defined in the related model
387
- else:
388
- return "one-to-many"
389
- elif field.one_to_many:
390
- return "one-to-many"
391
- elif field.many_to_many:
392
- return "many-to-many"
393
- elif field.one_to_one:
394
- return "one-to-one"
395
- else:
396
- return None
397
-
398
-
399
- class _SchemaHandler:
400
- def __init__(self) -> None:
401
- self.included_modules = ["core"] + list(settings.instance.modules)
402
- self.modules = self._get_modules_metadata()
403
-
404
- def to_dict(self, include_django_objects: bool = True):
405
- return {
406
- module_name if module_name != "lamindb" else "core": {
407
- model_name: model.to_dict(include_django_objects)
408
- for model_name, model in module.items()
409
- }
410
- for module_name, module in self.modules.items()
411
- }
412
-
413
- def to_json(self):
414
- return self.to_dict(include_django_objects=False)
415
-
416
- def _get_modules_metadata(self):
417
- from django.apps import apps
418
- from lamindb.models import Registry, SQLRecord
419
-
420
- all_models = {module_name: {} for module_name in self.included_modules}
421
-
422
- # Iterate through all registered Django models
423
- for model in apps.get_models(include_auto_created=True):
424
- # Check if model meets the criteria
425
- if model is not SQLRecord and not model._meta.abstract:
426
- module_name = Registry.__get_module_name__(model)
427
- # Only include if module is in our included list
428
- if module_name in self.included_modules:
429
- model_name = model._meta.model_name
430
- all_models[module_name][model_name] = _ModelHandler(
431
- model, module_name, self.included_modules
432
- )
433
-
434
- assert all_models
435
- return all_models
436
-
437
- def _get_module_set_info(self):
438
- # TODO: rely on schemamodule table for this
439
- module_set_info = []
440
- for module_name in self.included_modules:
441
- module = self._get_schema_module(module_name)
442
- if module_name == "lamindb":
443
- module_name = "core"
444
- module_set_info.append(
445
- {"id": 0, "name": module_name, "version": module.__version__}
446
- )
447
- return module_set_info
448
-
449
- @staticmethod
450
- def _get_schema_module(module_name):
451
- return importlib.import_module(get_schema_module_name(module_name))
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import importlib
5
+ import json
6
+ from typing import TYPE_CHECKING, Literal
7
+ from uuid import UUID
8
+
9
+ from lamin_utils import logger
10
+ from pydantic import BaseModel
11
+
12
+ from lamindb_setup import settings
13
+ from lamindb_setup._init_instance import get_schema_module_name
14
+ from lamindb_setup.core._hub_client import call_with_fallback_auth
15
+
16
+ # surpress pydantic warning about `model_` namespace
17
+ try:
18
+ BaseModel.model_config["protected_namespaces"] = ()
19
+ logger.debug(
20
+ "pydantic.BaseModel.model_config['protected_namespaces'] has been set to ()"
21
+ )
22
+ except Exception:
23
+ pass
24
+
25
+
26
+ if TYPE_CHECKING:
27
+ from django.db.models import (
28
+ Field,
29
+ ForeignKey,
30
+ ForeignObjectRel,
31
+ ManyToManyField,
32
+ ManyToManyRel,
33
+ ManyToOneRel,
34
+ OneToOneField,
35
+ OneToOneRel,
36
+ )
37
+ from supabase import Client
38
+
39
+
40
+ def update_schema_in_hub(access_token: str | None = None) -> tuple[bool, UUID, dict]:
41
+ return call_with_fallback_auth(_synchronize_schema, access_token=access_token)
42
+
43
+
44
+ def _synchronize_schema(client: Client) -> tuple[bool, UUID, dict]:
45
+ schema_metadata = _SchemaHandler()
46
+ schema_metadata_dict = schema_metadata.to_json()
47
+ schema_uuid = _dict_to_uuid(schema_metadata_dict)
48
+ schema = _get_schema_by_id(schema_uuid, client)
49
+
50
+ is_new = schema is None
51
+ if is_new:
52
+ module_set_info = schema_metadata._get_module_set_info()
53
+ module_ids = "-".join(str(module_info["id"]) for module_info in module_set_info)
54
+ schema = (
55
+ client.table("schema")
56
+ .insert(
57
+ {
58
+ "id": schema_uuid.hex,
59
+ "module_ids": module_ids,
60
+ "module_set_info": module_set_info,
61
+ "schema_json": schema_metadata_dict,
62
+ }
63
+ )
64
+ .execute()
65
+ .data[0]
66
+ )
67
+
68
+ instance_response = (
69
+ client.table("instance")
70
+ .update({"schema_id": schema_uuid.hex})
71
+ .eq("id", settings.instance._id.hex)
72
+ .execute()
73
+ )
74
+ assert len(instance_response.data) == 1, (
75
+ f"schema of instance {settings.instance._id.hex} could not be updated with schema {schema_uuid.hex}"
76
+ )
77
+
78
+ return is_new, schema_uuid, schema
79
+
80
+
81
+ def get_schema_by_id(id: UUID):
82
+ return call_with_fallback_auth(_get_schema_by_id, id=id)
83
+
84
+
85
+ def _get_schema_by_id(id: UUID, client: Client):
86
+ response = client.table("schema").select("*").eq("id", id.hex).execute()
87
+ if len(response.data) == 0:
88
+ return None
89
+ return response.data[0]
90
+
91
+
92
+ def _dict_to_uuid(dict: dict):
93
+ encoded = json.dumps(dict, sort_keys=True).encode("utf-8")
94
+ hash = hashlib.md5(encoded).digest()
95
+ uuid = UUID(bytes=hash[:16])
96
+ return uuid
97
+
98
+
99
+ RelationType = Literal["many-to-one", "one-to-many", "many-to-many", "one-to-one"]
100
+ Type = Literal[
101
+ "ForeignKey",
102
+ # the following are generated with `from django.db import models; [attr for attr in dir(models) if attr.endswith('Field')]`
103
+ "AutoField",
104
+ "BigAutoField",
105
+ "BigIntegerField",
106
+ "BinaryField",
107
+ "BooleanField",
108
+ "CharField",
109
+ "CommaSeparatedIntegerField",
110
+ "DateField",
111
+ "DateTimeField",
112
+ "DecimalField",
113
+ "DurationField",
114
+ "EmailField",
115
+ "Field",
116
+ "FileField",
117
+ "FilePathField",
118
+ "FloatField",
119
+ "GeneratedField",
120
+ "GenericIPAddressField",
121
+ "IPAddressField",
122
+ "ImageField",
123
+ "IntegerField",
124
+ "JSONField",
125
+ "ManyToManyField",
126
+ "NullBooleanField",
127
+ "OneToOneField",
128
+ "PositiveBigIntegerField",
129
+ "PositiveIntegerField",
130
+ "PositiveSmallIntegerField",
131
+ "SlugField",
132
+ "SmallAutoField",
133
+ "SmallIntegerField",
134
+ "TextField",
135
+ "TimeField",
136
+ "URLField",
137
+ "UUIDField",
138
+ ]
139
+
140
+
141
+ class Through(BaseModel):
142
+ left_key: str
143
+ right_key: str
144
+ link_table_name: str | None = None
145
+
146
+
147
+ class FieldMetadata(BaseModel):
148
+ type: Type
149
+ column_name: str | None = None
150
+ through: Through | None = None
151
+ field_name: str
152
+ model_name: str
153
+ schema_name: str
154
+ is_link_table: bool
155
+ is_primary_key: bool
156
+ is_editable: bool
157
+ max_length: int | None = None
158
+ relation_type: RelationType | None = None
159
+ related_field_name: str | None = None
160
+ related_model_name: str | None = None
161
+ related_schema_name: str | None = None
162
+
163
+
164
+ class _ModelHandler:
165
+ def __init__(self, model, module_name: str, included_modules: list[str]) -> None:
166
+ from lamindb.models import IsLink
167
+
168
+ self.model = model
169
+ self.class_name = model.__name__
170
+ self.module_name = module_name
171
+ self.model_name = model._meta.model_name
172
+ self.table_name = model._meta.db_table
173
+ self.included_modules = included_modules
174
+ self.fields = self._get_fields_metadata(self.model)
175
+ self.is_auto_created = bool(model._meta.auto_created)
176
+ self.is_link_table = issubclass(model, IsLink) or self.is_auto_created
177
+ self.name_field = model._name_field if hasattr(model, "_name_field") else None
178
+ self.ontology_id_field = (
179
+ model._ontology_id_field if hasattr(model, "_ontology_id_field") else None
180
+ )
181
+
182
+ def to_dict(self, include_django_objects: bool = True):
183
+ _dict = {
184
+ "fields": self.fields.copy(),
185
+ "class_name": self.class_name,
186
+ "table_name": self.table_name,
187
+ "is_auto_created": self.is_auto_created,
188
+ "is_link_table": self.is_link_table,
189
+ "name_field": self.name_field,
190
+ "ontology_id_field": self.ontology_id_field,
191
+ }
192
+
193
+ for field_name in self.fields.keys():
194
+ _dict["fields"][field_name] = _dict["fields"][field_name].__dict__
195
+ through = _dict["fields"][field_name]["through"]
196
+ if through is not None:
197
+ _dict["fields"][field_name]["through"] = through.__dict__
198
+
199
+ if include_django_objects:
200
+ _dict.update({"model": self.model})
201
+
202
+ return _dict
203
+
204
+ def _get_fields_metadata(self, model):
205
+ related_fields = []
206
+ fields_metadata: dict[str, FieldMetadata] = {}
207
+
208
+ for field in model._meta.get_fields():
209
+ field_metadata = self._get_field_metadata(model, field)
210
+ if field_metadata.related_schema_name is None:
211
+ fields_metadata.update({field.name: field_metadata})
212
+
213
+ if (
214
+ field_metadata.related_schema_name in self.included_modules
215
+ and field_metadata.schema_name in self.included_modules
216
+ ):
217
+ related_fields.append(field)
218
+
219
+ related_fields_metadata = self._get_related_fields_metadata(
220
+ model, related_fields
221
+ )
222
+
223
+ fields_metadata = {**fields_metadata, **related_fields_metadata}
224
+
225
+ return fields_metadata
226
+
227
+ def _get_related_fields_metadata(self, model, fields: list[ForeignObjectRel]):
228
+ related_fields: dict[str, FieldMetadata] = {}
229
+
230
+ for field in fields:
231
+ if field.many_to_one:
232
+ related_fields.update(
233
+ {f"{field.name}": self._get_field_metadata(model, field)}
234
+ )
235
+ elif field.one_to_many:
236
+ # exclude self reference as it is already included in the many to one
237
+ if field.related_model == model:
238
+ continue
239
+ related_fields.update(
240
+ {f"{field.name}": self._get_field_metadata(model, field.field)}
241
+ )
242
+ elif field.many_to_many:
243
+ related_fields.update(
244
+ {f"{field.name}": self._get_field_metadata(model, field)}
245
+ )
246
+ elif field.one_to_one:
247
+ related_fields.update(
248
+ {f"{field.name}": self._get_field_metadata(model, field)}
249
+ )
250
+
251
+ return related_fields
252
+
253
+ def _get_field_metadata(self, model, field: Field):
254
+ from lamindb.models import IsLink, Registry
255
+
256
+ internal_type = field.get_internal_type()
257
+ model_name = field.model._meta.model_name
258
+ relation_type = self._get_relation_type(model, field)
259
+
260
+ schema_name = Registry.__get_module_name__(field.model)
261
+
262
+ if field.related_model is None:
263
+ related_model_name = None
264
+ related_schema_name = None
265
+ related_field_name = None
266
+ is_editable = field.editable
267
+ max_length = field.max_length
268
+ else:
269
+ related_model_name = field.related_model._meta.model_name
270
+ related_schema_name = Registry.__get_module_name__(field.related_model)
271
+ related_field_name = field.remote_field.name
272
+ is_editable = False
273
+ max_length = None
274
+
275
+ field_name = field.name
276
+ is_primary_key = getattr(field, "primary_key", False)
277
+
278
+ if relation_type in ["one-to-many"]:
279
+ # For a one-to-many relation, the field belong
280
+ # to the other model as a foreign key.
281
+ # To make usage similar to other relation types
282
+ # we need to invert model and related model.
283
+ schema_name, related_schema_name = related_schema_name, schema_name
284
+ model_name, related_model_name = related_model_name, model_name
285
+ field_name, related_field_name = related_field_name, field_name
286
+ pass
287
+
288
+ column = None
289
+ if relation_type not in ["many-to-many", "one-to-many"]:
290
+ # have to reload it here in case reset happened
291
+ from django.db.models import ForeignObjectRel
292
+
293
+ if not isinstance(field, ForeignObjectRel):
294
+ column = field.column
295
+
296
+ if relation_type is None:
297
+ through = None
298
+ elif relation_type == "many-to-many":
299
+ through = self._get_through_many_to_many(field)
300
+ else:
301
+ through = self._get_through(field)
302
+
303
+ return FieldMetadata(
304
+ schema_name=schema_name if schema_name != "lamindb" else "core",
305
+ model_name=model_name,
306
+ field_name=field_name,
307
+ type=internal_type,
308
+ is_link_table=issubclass(field.model, IsLink),
309
+ is_primary_key=is_primary_key,
310
+ is_editable=is_editable,
311
+ max_length=max_length,
312
+ column_name=column,
313
+ relation_type=relation_type,
314
+ related_schema_name=related_schema_name
315
+ if related_schema_name != "lamindb"
316
+ else "core",
317
+ related_model_name=related_model_name,
318
+ related_field_name=related_field_name,
319
+ through=through,
320
+ )
321
+
322
+ @staticmethod
323
+ def _get_through_many_to_many(field_or_rel: ManyToManyField | ManyToManyRel):
324
+ # have to reload it here in case reset happened
325
+ from django.db.models import ManyToManyField, ManyToManyRel
326
+ from lamindb.models import Registry
327
+
328
+ if isinstance(field_or_rel, ManyToManyField):
329
+ if field_or_rel.model != Registry:
330
+ return Through(
331
+ left_key=field_or_rel.m2m_column_name(),
332
+ right_key=field_or_rel.m2m_reverse_name(),
333
+ link_table_name=field_or_rel.remote_field.through._meta.db_table,
334
+ )
335
+ else:
336
+ return Through(
337
+ left_key=field_or_rel.m2m_reverse_name(),
338
+ right_key=field_or_rel.m2m_column_name(),
339
+ link_table_name=field_or_rel.remote_field.through._meta.db_table,
340
+ )
341
+
342
+ if isinstance(field_or_rel, ManyToManyRel):
343
+ if field_or_rel.model != Registry:
344
+ return Through(
345
+ left_key=field_or_rel.field.m2m_reverse_name(),
346
+ right_key=field_or_rel.field.m2m_column_name(),
347
+ link_table_name=field_or_rel.through._meta.db_table,
348
+ )
349
+ else:
350
+ return Through(
351
+ left_key=field_or_rel.field.m2m_column_name(),
352
+ right_key=field_or_rel.field.m2m_reverse_name(),
353
+ link_table_name=field_or_rel.through._meta.db_table,
354
+ )
355
+
356
+ def _get_through(
357
+ self, field_or_rel: ForeignKey | OneToOneField | ManyToOneRel | OneToOneRel
358
+ ):
359
+ # have to reload it here in case reset happened
360
+ from django.db.models import ForeignObjectRel
361
+
362
+ if isinstance(field_or_rel, ForeignObjectRel):
363
+ rel_1 = field_or_rel.field.related_fields[0][0]
364
+ rel_2 = field_or_rel.field.related_fields[0][1]
365
+ else:
366
+ rel_1 = field_or_rel.related_fields[0][0]
367
+ rel_2 = field_or_rel.related_fields[0][1]
368
+
369
+ if rel_1.model._meta.model_name == self.model._meta.model_name:
370
+ return Through(
371
+ left_key=rel_1.column,
372
+ right_key=rel_2.column,
373
+ )
374
+ else:
375
+ return Through(
376
+ left_key=rel_2.column,
377
+ right_key=rel_1.column,
378
+ )
379
+
380
+ @staticmethod
381
+ def _get_relation_type(model, field: Field):
382
+ if field.many_to_one:
383
+ # defined in the model
384
+ if model == field.model:
385
+ return "many-to-one"
386
+ # defined in the related model
387
+ else:
388
+ return "one-to-many"
389
+ elif field.one_to_many:
390
+ return "one-to-many"
391
+ elif field.many_to_many:
392
+ return "many-to-many"
393
+ elif field.one_to_one:
394
+ return "one-to-one"
395
+ else:
396
+ return None
397
+
398
+
399
+ class _SchemaHandler:
400
+ def __init__(self) -> None:
401
+ self.included_modules = ["core"] + list(settings.instance.modules)
402
+ self.modules = self._get_modules_metadata()
403
+
404
+ def to_dict(self, include_django_objects: bool = True):
405
+ return {
406
+ module_name if module_name != "lamindb" else "core": {
407
+ model_name: model.to_dict(include_django_objects)
408
+ for model_name, model in module.items()
409
+ }
410
+ for module_name, module in self.modules.items()
411
+ }
412
+
413
+ def to_json(self):
414
+ return self.to_dict(include_django_objects=False)
415
+
416
+ def _get_modules_metadata(self):
417
+ from django.apps import apps
418
+ from lamindb.models import Registry, SQLRecord
419
+
420
+ all_models = {module_name: {} for module_name in self.included_modules}
421
+
422
+ # Iterate through all registered Django models
423
+ for model in apps.get_models(include_auto_created=True):
424
+ # Check if model meets the criteria
425
+ if model is not SQLRecord and not model._meta.abstract:
426
+ module_name = Registry.__get_module_name__(model)
427
+ # Only include if module is in our included list
428
+ if module_name in self.included_modules:
429
+ model_name = model._meta.model_name
430
+ all_models[module_name][model_name] = _ModelHandler(
431
+ model, module_name, self.included_modules
432
+ )
433
+
434
+ assert all_models
435
+ return all_models
436
+
437
+ def _get_module_set_info(self):
438
+ # TODO: rely on schemamodule table for this
439
+ module_set_info = []
440
+ for module_name in self.included_modules:
441
+ module = self._get_schema_module(module_name)
442
+ if module_name == "lamindb":
443
+ module_name = "core"
444
+ module_set_info.append(
445
+ {"id": 0, "name": module_name, "version": module.__version__}
446
+ )
447
+ return module_set_info
448
+
449
+ @staticmethod
450
+ def _get_schema_module(module_name):
451
+ return importlib.import_module(get_schema_module_name(module_name))