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