lamindb_setup 0.78.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lamindb_setup/__init__.py +74 -0
- lamindb_setup/_cache.py +48 -0
- lamindb_setup/_check.py +7 -0
- lamindb_setup/_check_setup.py +92 -0
- lamindb_setup/_close.py +35 -0
- lamindb_setup/_connect_instance.py +429 -0
- lamindb_setup/_delete.py +141 -0
- lamindb_setup/_django.py +39 -0
- lamindb_setup/_entry_points.py +22 -0
- lamindb_setup/_exportdb.py +68 -0
- lamindb_setup/_importdb.py +50 -0
- lamindb_setup/_init_instance.py +411 -0
- lamindb_setup/_migrate.py +239 -0
- lamindb_setup/_register_instance.py +36 -0
- lamindb_setup/_schema.py +27 -0
- lamindb_setup/_schema_metadata.py +411 -0
- lamindb_setup/_set_managed_storage.py +55 -0
- lamindb_setup/_setup_user.py +137 -0
- lamindb_setup/_silence_loggers.py +44 -0
- lamindb_setup/core/__init__.py +21 -0
- lamindb_setup/core/_aws_credentials.py +151 -0
- lamindb_setup/core/_aws_storage.py +48 -0
- lamindb_setup/core/_deprecated.py +55 -0
- lamindb_setup/core/_docs.py +14 -0
- lamindb_setup/core/_hub_client.py +173 -0
- lamindb_setup/core/_hub_core.py +554 -0
- lamindb_setup/core/_hub_crud.py +211 -0
- lamindb_setup/core/_hub_utils.py +109 -0
- lamindb_setup/core/_private_django_api.py +88 -0
- lamindb_setup/core/_settings.py +184 -0
- lamindb_setup/core/_settings_instance.py +485 -0
- lamindb_setup/core/_settings_load.py +117 -0
- lamindb_setup/core/_settings_save.py +92 -0
- lamindb_setup/core/_settings_storage.py +350 -0
- lamindb_setup/core/_settings_store.py +75 -0
- lamindb_setup/core/_settings_user.py +55 -0
- lamindb_setup/core/_setup_bionty_sources.py +101 -0
- lamindb_setup/core/cloud_sqlite_locker.py +237 -0
- lamindb_setup/core/django.py +115 -0
- lamindb_setup/core/exceptions.py +10 -0
- lamindb_setup/core/hashing.py +116 -0
- lamindb_setup/core/types.py +17 -0
- lamindb_setup/core/upath.py +779 -0
- lamindb_setup-0.78.0.dist-info/LICENSE +201 -0
- lamindb_setup-0.78.0.dist-info/METADATA +47 -0
- lamindb_setup-0.78.0.dist-info/RECORD +47 -0
- lamindb_setup-0.78.0.dist-info/WHEEL +4 -0
lamindb_setup/_schema.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.urls import path
|
|
4
|
+
from lamin_utils import logger
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from schema_graph.views import Schema
|
|
8
|
+
except ImportError:
|
|
9
|
+
logger.error("to view the schema: pip install django-schema-graph")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
urlpatterns = [
|
|
13
|
+
path("schema/", Schema.as_view()),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def view():
|
|
18
|
+
from django.core.management import call_command
|
|
19
|
+
|
|
20
|
+
from ._check_setup import _check_instance_setup
|
|
21
|
+
from .core._settings import settings
|
|
22
|
+
from .core.django import setup_django
|
|
23
|
+
|
|
24
|
+
if _check_instance_setup():
|
|
25
|
+
raise RuntimeError("Restart Python session or use CLI!")
|
|
26
|
+
setup_django(settings.instance, view_schema=True)
|
|
27
|
+
call_command("runserver")
|
|
@@ -0,0 +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(access_token: str | None = None) -> tuple[bool, UUID, dict]:
|
|
37
|
+
return call_with_fallback_auth(_synchronize_schema, access_token=access_token)
|
|
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))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from lamin_utils import logger
|
|
6
|
+
|
|
7
|
+
from ._init_instance import register_storage_in_instance
|
|
8
|
+
from .core._hub_core import delete_storage_record
|
|
9
|
+
from .core._settings import settings
|
|
10
|
+
from .core._settings_storage import init_storage
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from lamindb_setup.core.types import UPathStr
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_managed_storage(root: UPathStr, **fs_kwargs):
|
|
17
|
+
"""Add or switch to another managed storage location.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
root: `UPathStr` - The new storage root, e.g., an S3 bucket.
|
|
21
|
+
**fs_kwargs: Additional fsspec arguments for cloud root, e.g., profile.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
if settings.instance.dialect == "sqlite":
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"Can't add additional managed storage locations for sqlite instances."
|
|
27
|
+
)
|
|
28
|
+
if not settings.instance.is_on_hub:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
"Can't add additional managed storage locations for instances that aren't managed through the hub."
|
|
31
|
+
)
|
|
32
|
+
# here the storage is registered in the hub
|
|
33
|
+
# hub_record_status="hub-record-created" if a new record is created
|
|
34
|
+
# "hub-record-retireved" if the storage is in the hub already
|
|
35
|
+
ssettings, hub_record_status = init_storage(
|
|
36
|
+
root=root, instance_id=settings.instance._id, register_hub=True
|
|
37
|
+
)
|
|
38
|
+
if ssettings._instance_id is None:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Cannot manage storage without write access: {ssettings.root}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# here the storage is saved in the instance
|
|
44
|
+
# if any error happens the record in the hub is deleted
|
|
45
|
+
# if it was created earlier and not retrieved
|
|
46
|
+
try:
|
|
47
|
+
register_storage_in_instance(ssettings)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
if hub_record_status == "hub-record-created" and ssettings._uuid is not None:
|
|
50
|
+
delete_storage_record(ssettings._uuid) # type: ignore
|
|
51
|
+
raise e
|
|
52
|
+
|
|
53
|
+
settings.instance._storage = ssettings
|
|
54
|
+
settings.instance._persist() # this also updates the settings object
|
|
55
|
+
settings.storage._set_fs_kwargs(**fs_kwargs)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from lamin_utils import logger
|
|
7
|
+
|
|
8
|
+
from ._check_setup import _check_instance_setup
|
|
9
|
+
from ._init_instance import register_user
|
|
10
|
+
from .core._settings import settings
|
|
11
|
+
from .core._settings_load import load_or_create_user_settings, load_user_settings
|
|
12
|
+
from .core._settings_save import save_user_settings
|
|
13
|
+
from .core._settings_store import (
|
|
14
|
+
current_user_settings_file,
|
|
15
|
+
user_settings_file_email,
|
|
16
|
+
user_settings_file_handle,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from lamindb_setup.core._settings_save import UserSettings
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_user(email: str | None = None, handle: str | None = None) -> UserSettings:
|
|
24
|
+
if email is not None:
|
|
25
|
+
settings_file = user_settings_file_email(email)
|
|
26
|
+
if handle is not None:
|
|
27
|
+
settings_file = user_settings_file_handle(handle)
|
|
28
|
+
if settings_file.exists():
|
|
29
|
+
user_settings = load_user_settings(settings_file)
|
|
30
|
+
save_user_settings(user_settings) # needed to save to current_user.env
|
|
31
|
+
assert user_settings.email is not None or user_settings.api_key is not None
|
|
32
|
+
else:
|
|
33
|
+
user_settings = load_or_create_user_settings()
|
|
34
|
+
if email is None:
|
|
35
|
+
raise SystemExit(
|
|
36
|
+
"✗ Use your email for your first login in a compute environment. "
|
|
37
|
+
"After that, you can use your handle."
|
|
38
|
+
)
|
|
39
|
+
user_settings.email = email
|
|
40
|
+
user_settings.handle = handle
|
|
41
|
+
save_user_settings(user_settings)
|
|
42
|
+
|
|
43
|
+
from .core._settings import settings
|
|
44
|
+
|
|
45
|
+
settings._user_settings = None # this is to refresh a settings instance
|
|
46
|
+
|
|
47
|
+
return user_settings
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def login(
|
|
51
|
+
user: str | None = None, *, key: str | None = None, api_key: str | None = None
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Log in user.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
user: handle or email
|
|
57
|
+
key: API key
|
|
58
|
+
api_key: Beta API key
|
|
59
|
+
"""
|
|
60
|
+
if user is None and api_key is None:
|
|
61
|
+
if "LAMIN_API_KEY" in os.environ:
|
|
62
|
+
api_key = os.environ["LAMIN_API_KEY"]
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError("Both `user` and `api_key` should not be `None`.")
|
|
65
|
+
|
|
66
|
+
if api_key is None:
|
|
67
|
+
if "@" in user: # type: ignore
|
|
68
|
+
email, handle = user, None
|
|
69
|
+
else:
|
|
70
|
+
email, handle = None, user
|
|
71
|
+
user_settings = load_user(email, handle)
|
|
72
|
+
|
|
73
|
+
if key is not None:
|
|
74
|
+
# within UserSettings, we still call it "password" for a while
|
|
75
|
+
user_settings.password = key
|
|
76
|
+
|
|
77
|
+
if user_settings.password is None:
|
|
78
|
+
api_key = user_settings.api_key
|
|
79
|
+
if api_key is None:
|
|
80
|
+
raise SystemExit(
|
|
81
|
+
"✗ No stored API key, please call: "
|
|
82
|
+
"`lamin login` or `lamin login <your-email> --key <API-key>`"
|
|
83
|
+
)
|
|
84
|
+
elif user_settings.email is None:
|
|
85
|
+
raise SystemExit(f"✗ No stored user email, please call: lamin login {user}")
|
|
86
|
+
else:
|
|
87
|
+
user_settings = load_or_create_user_settings()
|
|
88
|
+
|
|
89
|
+
from .core._hub_core import sign_in_hub, sign_in_hub_api_key
|
|
90
|
+
|
|
91
|
+
if api_key is None:
|
|
92
|
+
response = sign_in_hub(
|
|
93
|
+
user_settings.email, # type: ignore
|
|
94
|
+
user_settings.password, # type: ignore
|
|
95
|
+
user_settings.handle,
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
response = sign_in_hub_api_key(api_key)
|
|
99
|
+
user_settings.password = None
|
|
100
|
+
|
|
101
|
+
if isinstance(response, Exception):
|
|
102
|
+
raise response
|
|
103
|
+
elif isinstance(response, str):
|
|
104
|
+
raise SystemExit(f"✗ Unsuccessful login: {response}.")
|
|
105
|
+
else:
|
|
106
|
+
user_uuid, user_id, user_handle, user_name, access_token = response
|
|
107
|
+
if api_key is not None:
|
|
108
|
+
logger.success(
|
|
109
|
+
f"logged in with API key (handle: {user_handle}, uid: {user_id})"
|
|
110
|
+
)
|
|
111
|
+
elif handle is None:
|
|
112
|
+
logger.success(f"logged in with handle {user_handle} (uid: {user_id})")
|
|
113
|
+
else:
|
|
114
|
+
logger.success(f"logged in with email {user_settings.email} (uid: {user_id})")
|
|
115
|
+
user_settings.uid = user_id
|
|
116
|
+
user_settings.handle = user_handle
|
|
117
|
+
user_settings.name = user_name
|
|
118
|
+
user_settings._uuid = user_uuid
|
|
119
|
+
user_settings.access_token = access_token
|
|
120
|
+
user_settings.api_key = api_key
|
|
121
|
+
save_user_settings(user_settings)
|
|
122
|
+
|
|
123
|
+
if settings._instance_exists and _check_instance_setup():
|
|
124
|
+
register_user(user_settings)
|
|
125
|
+
|
|
126
|
+
settings._user_settings = None
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def logout():
|
|
131
|
+
if current_user_settings_file().exists():
|
|
132
|
+
current_user_settings_file().unlink()
|
|
133
|
+
# update user info
|
|
134
|
+
settings._user_settings = None
|
|
135
|
+
logger.success("logged out")
|
|
136
|
+
else:
|
|
137
|
+
logger.important("already logged out")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
silenced = False
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# from https://github.com/boto/boto3/blob/8c6e641bed8130a9d8cb4d97b4acbe7aa0d0657a/boto3/__init__.py#L37
|
|
9
|
+
def set_stream_logger(name, level):
|
|
10
|
+
logger = logging.getLogger(name)
|
|
11
|
+
logger.setLevel(level)
|
|
12
|
+
handler = logging.StreamHandler()
|
|
13
|
+
handler.setLevel(level)
|
|
14
|
+
logger.addHandler(handler)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def silence_loggers():
|
|
18
|
+
global silenced
|
|
19
|
+
|
|
20
|
+
if not silenced:
|
|
21
|
+
# this gets logged 6 times
|
|
22
|
+
set_stream_logger(name="botocore.credentials", level=logging.WARNING)
|
|
23
|
+
set_stream_logger(name="botocore.hooks", level=logging.WARNING)
|
|
24
|
+
set_stream_logger(name="botocore.utils", level=logging.WARNING)
|
|
25
|
+
set_stream_logger(name="botocore.auth", level=logging.WARNING)
|
|
26
|
+
set_stream_logger(name="botocore.endpoint", level=logging.WARNING)
|
|
27
|
+
set_stream_logger(name="httpx", level=logging.WARNING)
|
|
28
|
+
try:
|
|
29
|
+
import aiobotocore
|
|
30
|
+
|
|
31
|
+
# the 7th logging message of credentials came from aiobotocore
|
|
32
|
+
set_stream_logger(name="aiobotocore.credentials", level=logging.WARNING)
|
|
33
|
+
except ImportError:
|
|
34
|
+
pass
|
|
35
|
+
try:
|
|
36
|
+
# google also aggressively logs authentication related warnings
|
|
37
|
+
# in cases where users access public data
|
|
38
|
+
set_stream_logger(name="google.auth._default", level=logging.ERROR)
|
|
39
|
+
set_stream_logger(
|
|
40
|
+
name="google.auth.compute_engine._metadata", level=logging.ERROR
|
|
41
|
+
)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
silenced = True
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Core setup library.
|
|
2
|
+
|
|
3
|
+
Settings:
|
|
4
|
+
|
|
5
|
+
.. autosummary::
|
|
6
|
+
:toctree:
|
|
7
|
+
|
|
8
|
+
SetupSettings
|
|
9
|
+
UserSettings
|
|
10
|
+
InstanceSettings
|
|
11
|
+
StorageSettings
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from . import django, types, upath
|
|
16
|
+
from ._deprecated import deprecated
|
|
17
|
+
from ._docs import doc_args
|
|
18
|
+
from ._settings import SetupSettings
|
|
19
|
+
from ._settings_instance import InstanceSettings
|
|
20
|
+
from ._settings_storage import StorageSettings
|
|
21
|
+
from ._settings_user import UserSettings
|