lamindb_setup 0.73.3__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 +1 -1
- lamindb_setup/_schema_metadata.py +178 -279
- lamindb_setup/core/_hub_client.py +2 -2
- lamindb_setup/core/_private_django_api.py +88 -0
- lamindb_setup/core/_settings.py +33 -4
- lamindb_setup/core/_settings_instance.py +2 -1
- lamindb_setup/core/hashing.py +29 -2
- {lamindb_setup-0.73.3.dist-info → lamindb_setup-0.74.0.dist-info}/METADATA +2 -2
- {lamindb_setup-0.73.3.dist-info → lamindb_setup-0.74.0.dist-info}/RECORD +11 -10
- {lamindb_setup-0.73.3.dist-info → lamindb_setup-0.74.0.dist-info}/LICENSE +0 -0
- {lamindb_setup-0.73.3.dist-info → lamindb_setup-0.74.0.dist-info}/WHEEL +0 -0
lamindb_setup/__init__.py
CHANGED
|
@@ -3,31 +3,26 @@ from __future__ import annotations
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import importlib
|
|
5
5
|
import json
|
|
6
|
-
from
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
ManyToOneRel,
|
|
16
|
+
OneToOneField,
|
|
17
|
+
OneToOneRel,
|
|
20
18
|
)
|
|
21
|
-
from
|
|
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 =
|
|
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
|
-
"
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
119
|
+
model_name: str
|
|
120
|
+
schema_name: str
|
|
152
121
|
is_link_table: bool
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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,
|
|
172
|
+
model, related_fields
|
|
246
173
|
)
|
|
247
174
|
|
|
248
175
|
fields_metadata = {**fields_metadata, **related_fields_metadata}
|
|
249
176
|
|
|
250
|
-
return fields_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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if relation_type
|
|
340
|
-
through =
|
|
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
|
|
358
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
395
|
-
def __init__(
|
|
396
|
-
self
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
476
|
-
def
|
|
477
|
-
|
|
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.
|
|
123
|
-
"
|
|
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)
|
lamindb_setup/core/_settings.py
CHANGED
|
@@ -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,9 +55,30 @@ 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
|
-
"""
|
|
81
|
+
"""Settings of current user."""
|
|
54
82
|
env_changed = (
|
|
55
83
|
self._user_settings_env is not None
|
|
56
84
|
and self._user_settings_env != get_env_name()
|
|
@@ -64,7 +92,7 @@ class SetupSettings:
|
|
|
64
92
|
|
|
65
93
|
@property
|
|
66
94
|
def instance(self) -> InstanceSettings:
|
|
67
|
-
"""
|
|
95
|
+
"""Settings of current LaminDB instance."""
|
|
68
96
|
env_changed = (
|
|
69
97
|
self._instance_settings_env is not None
|
|
70
98
|
and self._instance_settings_env != get_env_name()
|
|
@@ -76,7 +104,7 @@ class SetupSettings:
|
|
|
76
104
|
|
|
77
105
|
@property
|
|
78
106
|
def storage(self) -> StorageSettings:
|
|
79
|
-
"""
|
|
107
|
+
"""Settings of default storage."""
|
|
80
108
|
return self.instance.storage
|
|
81
109
|
|
|
82
110
|
@property
|
|
@@ -92,6 +120,7 @@ class SetupSettings:
|
|
|
92
120
|
"""Rich string representation."""
|
|
93
121
|
repr = self.user.__repr__()
|
|
94
122
|
repr += f"\nAuto-connect in Python: {self.auto_connect}\n"
|
|
123
|
+
repr += f"Private Django API: {self.private_django_api}\n"
|
|
95
124
|
if self._instance_exists:
|
|
96
125
|
repr += self.instance.__repr__()
|
|
97
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
|
-
|
|
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
|
lamindb_setup/core/hashing.py
CHANGED
|
@@ -12,7 +12,10 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import base64
|
|
14
14
|
import hashlib
|
|
15
|
-
from
|
|
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:
|
|
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.
|
|
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.
|
|
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=
|
|
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
|
|
@@ -12,7 +12,7 @@ lamindb_setup/_init_instance.py,sha256=e6OJSpN2eJZd22kAfUVEUbqXF48FzNfLc5xMTS8gZ
|
|
|
12
12
|
lamindb_setup/_migrate.py,sha256=P4n3x0SYzO9szjF2-JMa7z4mQadtWjHv5ow4HbCDZLI,8864
|
|
13
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=
|
|
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,12 +21,13 @@ 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=
|
|
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/
|
|
29
|
-
lamindb_setup/core/
|
|
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
33
|
lamindb_setup/core/_settings_storage.py,sha256=65aobewYX6VfOeYZjZQOOI7ZD_3b4QA9TDmrduU0m4c,13262
|
|
@@ -36,10 +37,10 @@ lamindb_setup/core/_setup_bionty_sources.py,sha256=h_pBANsSGK6ujAFsG21mtADHVJoML
|
|
|
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=
|
|
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.
|
|
43
|
-
lamindb_setup-0.
|
|
44
|
-
lamindb_setup-0.
|
|
45
|
-
lamindb_setup-0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|