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.
Files changed (47) hide show
  1. lamindb_setup/__init__.py +74 -0
  2. lamindb_setup/_cache.py +48 -0
  3. lamindb_setup/_check.py +7 -0
  4. lamindb_setup/_check_setup.py +92 -0
  5. lamindb_setup/_close.py +35 -0
  6. lamindb_setup/_connect_instance.py +429 -0
  7. lamindb_setup/_delete.py +141 -0
  8. lamindb_setup/_django.py +39 -0
  9. lamindb_setup/_entry_points.py +22 -0
  10. lamindb_setup/_exportdb.py +68 -0
  11. lamindb_setup/_importdb.py +50 -0
  12. lamindb_setup/_init_instance.py +411 -0
  13. lamindb_setup/_migrate.py +239 -0
  14. lamindb_setup/_register_instance.py +36 -0
  15. lamindb_setup/_schema.py +27 -0
  16. lamindb_setup/_schema_metadata.py +411 -0
  17. lamindb_setup/_set_managed_storage.py +55 -0
  18. lamindb_setup/_setup_user.py +137 -0
  19. lamindb_setup/_silence_loggers.py +44 -0
  20. lamindb_setup/core/__init__.py +21 -0
  21. lamindb_setup/core/_aws_credentials.py +151 -0
  22. lamindb_setup/core/_aws_storage.py +48 -0
  23. lamindb_setup/core/_deprecated.py +55 -0
  24. lamindb_setup/core/_docs.py +14 -0
  25. lamindb_setup/core/_hub_client.py +173 -0
  26. lamindb_setup/core/_hub_core.py +554 -0
  27. lamindb_setup/core/_hub_crud.py +211 -0
  28. lamindb_setup/core/_hub_utils.py +109 -0
  29. lamindb_setup/core/_private_django_api.py +88 -0
  30. lamindb_setup/core/_settings.py +184 -0
  31. lamindb_setup/core/_settings_instance.py +485 -0
  32. lamindb_setup/core/_settings_load.py +117 -0
  33. lamindb_setup/core/_settings_save.py +92 -0
  34. lamindb_setup/core/_settings_storage.py +350 -0
  35. lamindb_setup/core/_settings_store.py +75 -0
  36. lamindb_setup/core/_settings_user.py +55 -0
  37. lamindb_setup/core/_setup_bionty_sources.py +101 -0
  38. lamindb_setup/core/cloud_sqlite_locker.py +237 -0
  39. lamindb_setup/core/django.py +115 -0
  40. lamindb_setup/core/exceptions.py +10 -0
  41. lamindb_setup/core/hashing.py +116 -0
  42. lamindb_setup/core/types.py +17 -0
  43. lamindb_setup/core/upath.py +779 -0
  44. lamindb_setup-0.78.0.dist-info/LICENSE +201 -0
  45. lamindb_setup-0.78.0.dist-info/METADATA +47 -0
  46. lamindb_setup-0.78.0.dist-info/RECORD +47 -0
  47. lamindb_setup-0.78.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,211 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+ from uuid import UUID, uuid4
5
+
6
+ from lamin_utils import logger
7
+ from supabase.client import Client # noqa
8
+
9
+
10
+ def select_instance_by_owner_name(
11
+ owner: str,
12
+ name: str,
13
+ client: Client,
14
+ ) -> dict | None:
15
+ try:
16
+ data = (
17
+ client.table("instance")
18
+ .select(
19
+ "*, account!inner!instance_account_id_28936e8f_fk_account_id(*),"
20
+ " storage!inner!storage_instance_id_359fca71_fk_instance_id(*)"
21
+ )
22
+ .eq("name", name)
23
+ .eq("account.handle", owner)
24
+ .eq("storage.is_default", True)
25
+ .execute()
26
+ .data
27
+ )
28
+ except Exception:
29
+ return None
30
+ if len(data) == 0:
31
+ return None
32
+ result = data[0]
33
+ # this is now a list
34
+ # assume only one default storage
35
+ result["storage"] = result["storage"][0]
36
+ return result
37
+
38
+
39
+ # --------------- ACCOUNT ----------------------
40
+ def select_account_by_handle(
41
+ handle: str,
42
+ client: Client,
43
+ ):
44
+ data = client.table("account").select("*").eq("handle", handle).execute().data
45
+ if len(data) == 0:
46
+ return None
47
+ return data[0]
48
+
49
+
50
+ def select_account_handle_name_by_lnid(lnid: str, client: Client):
51
+ data = (
52
+ client.table("account").select("handle, name").eq("lnid", lnid).execute().data
53
+ )
54
+ if not data:
55
+ return None
56
+ return data[0]
57
+
58
+
59
+ # --------------- INSTANCE ----------------------
60
+ def select_instance_by_name(
61
+ account_id: str,
62
+ name: str,
63
+ client: Client,
64
+ ):
65
+ data = (
66
+ client.table("instance")
67
+ .select("*")
68
+ .eq("account_id", account_id)
69
+ .eq("name", name)
70
+ .execute()
71
+ .data
72
+ )
73
+ if len(data) == 0:
74
+ return None
75
+ return data[0]
76
+
77
+
78
+ def select_instance_by_id(
79
+ instance_id: str,
80
+ client: Client,
81
+ ):
82
+ response = client.table("instance").select("*").eq("id", instance_id).execute()
83
+ if len(response.data) == 0:
84
+ return None
85
+ return response.data[0]
86
+
87
+
88
+ def select_instance_by_id_with_storage(
89
+ instance_id: str,
90
+ client: Client,
91
+ ):
92
+ response = (
93
+ client.table("instance")
94
+ .select("*, storage!instance_storage_id_87963cc8_fk_storage_id(*)")
95
+ .eq("id", instance_id)
96
+ .execute()
97
+ )
98
+ if len(response.data) == 0:
99
+ return None
100
+ return response.data[0]
101
+
102
+
103
+ def update_instance(instance_id: str, instance_fields: dict, client: Client):
104
+ response = (
105
+ client.table("instance").update(instance_fields).eq("id", instance_id).execute()
106
+ )
107
+ if len(response.data) == 0:
108
+ raise PermissionError(
109
+ f"Update of instance with {instance_id} was not successful. Probably, you"
110
+ " don't have sufficient permissions."
111
+ )
112
+ return response.data[0]
113
+
114
+
115
+ # --------------- COLLABORATOR ----------------------
116
+
117
+
118
+ def select_collaborator(
119
+ instance_id: str,
120
+ account_id: str,
121
+ client: Client,
122
+ ):
123
+ data = (
124
+ client.table("account_instance")
125
+ .select("*")
126
+ .eq("instance_id", instance_id)
127
+ .eq("account_id", account_id)
128
+ .execute()
129
+ .data
130
+ )
131
+ if len(data) == 0:
132
+ return None
133
+ return data[0]
134
+
135
+
136
+ # --------------- STORAGE ----------------------
137
+
138
+
139
+ def select_default_storage_by_instance_id(
140
+ instance_id: str, client: Client
141
+ ) -> dict | None:
142
+ try:
143
+ data = (
144
+ client.table("storage")
145
+ .select("*")
146
+ .eq("instance_id", instance_id)
147
+ .eq("is_default", True)
148
+ .execute()
149
+ .data
150
+ )
151
+ except Exception:
152
+ return None
153
+ if len(data) == 0:
154
+ return None
155
+ return data[0]
156
+
157
+
158
+ # --------------- DBUser ----------------------
159
+
160
+
161
+ def insert_db_user(
162
+ *,
163
+ name: str,
164
+ db_user_name: str,
165
+ db_user_password: str,
166
+ instance_id: UUID,
167
+ client: Client,
168
+ ) -> None:
169
+ fields = (
170
+ {
171
+ "id": uuid4().hex,
172
+ "instance_id": instance_id.hex,
173
+ "name": name,
174
+ "db_user_name": db_user_name,
175
+ "db_user_password": db_user_password,
176
+ },
177
+ )
178
+ data = client.table("db_user").insert(fields).execute().data
179
+ return data[0]
180
+
181
+
182
+ def select_db_user_by_instance(instance_id: str, client: Client):
183
+ """Get db_user for which client has permission."""
184
+ data = (
185
+ client.table("db_user")
186
+ .select("*")
187
+ .eq("instance_id", instance_id)
188
+ .execute()
189
+ .data
190
+ )
191
+ if len(data) == 0:
192
+ return None
193
+ elif len(data) > 1:
194
+ for item in data:
195
+ if item["name"] == "write":
196
+ return item
197
+ logger.warning("found multiple db credentials, using the first one")
198
+ return data[0]
199
+
200
+
201
+ def _delete_instance_record(instance_id: UUID, client: Client) -> None:
202
+ if not isinstance(instance_id, UUID):
203
+ instance_id = UUID(instance_id)
204
+ response = client.table("instance").delete().eq("id", instance_id.hex).execute()
205
+ if response.data:
206
+ logger.important(f"deleted instance record on hub {instance_id.hex}")
207
+ else:
208
+ raise PermissionError(
209
+ f"Deleting of instance with {instance_id.hex} was not successful. Probably, you"
210
+ " don't have sufficient permissions."
211
+ )
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, ClassVar
4
+ from urllib.parse import urlparse, urlunparse
5
+
6
+ from pydantic import BaseModel, Field, GetCoreSchemaHandler
7
+ from pydantic_core import CoreSchema, core_schema
8
+
9
+
10
+ def validate_schema_arg(schema: str | None = None) -> str:
11
+ if schema is None or schema == "":
12
+ return ""
13
+ # currently no actual validation, can add back if we see a need
14
+ # the following just strips white spaces
15
+ to_be_validated = [s.strip() for s in schema.split(",")]
16
+ return ",".join(to_be_validated)
17
+
18
+
19
+ def validate_db_arg(db: str | None) -> None:
20
+ if db is not None:
21
+ LaminDsnModel(db=db)
22
+
23
+
24
+ class LaminDsn(str):
25
+ allowed_schemes: ClassVar[set[str]] = {
26
+ "postgresql",
27
+ # future enabled schemes
28
+ # "snowflake",
29
+ # "bigquery"
30
+ }
31
+
32
+ @classmethod
33
+ def __get_pydantic_core_schema__(
34
+ cls, source_type: Any, handler: GetCoreSchemaHandler
35
+ ) -> CoreSchema:
36
+ return core_schema.no_info_after_validator_function(
37
+ cls.validate,
38
+ core_schema.str_schema(),
39
+ serialization=core_schema.plain_serializer_function_ser_schema(str),
40
+ )
41
+
42
+ @classmethod
43
+ def validate(cls, v: Any) -> LaminDsn:
44
+ if isinstance(v, str):
45
+ parsed = urlparse(v)
46
+ if parsed.scheme not in cls.allowed_schemes:
47
+ raise ValueError(f"Invalid scheme: {parsed.scheme}")
48
+ return cls(v)
49
+ elif isinstance(v, cls):
50
+ return v
51
+ else:
52
+ raise ValueError(f"Invalid value for LaminDsn: {v}")
53
+
54
+ @property
55
+ def user(self) -> str | None:
56
+ return urlparse(self).username
57
+
58
+ @property
59
+ def password(self) -> str | None:
60
+ return urlparse(self).password
61
+
62
+ @property
63
+ def host(self) -> str | None:
64
+ return urlparse(self).hostname
65
+
66
+ @property
67
+ def port(self) -> int | None:
68
+ return urlparse(self).port
69
+
70
+ @property
71
+ def database(self) -> str:
72
+ return urlparse(self).path.lstrip("/")
73
+
74
+ @property
75
+ def scheme(self) -> str:
76
+ return urlparse(self).scheme
77
+
78
+ @classmethod
79
+ def build(
80
+ cls,
81
+ *,
82
+ scheme: str,
83
+ user: str | None = None,
84
+ password: str | None = None,
85
+ host: str,
86
+ port: int | None = None,
87
+ database: str | None = None,
88
+ query: str | None = None,
89
+ fragment: str | None = None,
90
+ ) -> LaminDsn:
91
+ netloc = host
92
+ if port is not None:
93
+ netloc = f"{netloc}:{port}"
94
+ if user is not None:
95
+ auth = user
96
+ if password is not None:
97
+ auth = f"{auth}:{password}"
98
+ netloc = f"{auth}@{netloc}"
99
+
100
+ path = f"/{database}" if database else ""
101
+
102
+ url = urlunparse((scheme, netloc, path, "", query or "", fragment or ""))
103
+ return cls(url)
104
+
105
+
106
+ class LaminDsnModel(BaseModel):
107
+ db: LaminDsn = Field(..., description="The database DSN")
108
+
109
+ model_config = {"arbitrary_types_allowed": True}
@@ -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)
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import TYPE_CHECKING
5
+
6
+ from appdirs import AppDirs
7
+
8
+ from ._settings_load import (
9
+ load_instance_settings,
10
+ load_or_create_user_settings,
11
+ load_system_storage_settings,
12
+ )
13
+ from ._settings_store import current_instance_settings_file, settings_dir
14
+ from .upath import LocalPathClasses, UPath
15
+
16
+ if TYPE_CHECKING:
17
+ from pathlib import Path
18
+
19
+ from lamindb_setup.core import InstanceSettings, StorageSettings, UserSettings
20
+
21
+ from .types import UPathStr
22
+
23
+
24
+ DEFAULT_CACHE_DIR = UPath(AppDirs("lamindb", "laminlabs").user_cache_dir)
25
+
26
+
27
+ def _process_cache_path(cache_path: UPathStr | None):
28
+ if cache_path is None or cache_path == "null":
29
+ return None
30
+ cache_dir = UPath(cache_path)
31
+ if not isinstance(cache_dir, LocalPathClasses):
32
+ raise ValueError("cache dir should be a local path.")
33
+ if cache_dir.exists() and not cache_dir.is_dir():
34
+ raise ValueError("cache dir should be a directory.")
35
+ return cache_dir
36
+
37
+
38
+ class SetupSettings:
39
+ """Setup settings."""
40
+
41
+ _using_key: str | None = None # set through lamindb.settings
42
+
43
+ _user_settings: UserSettings | None = None
44
+ _instance_settings: InstanceSettings | None = None
45
+
46
+ _user_settings_env: str | None = None
47
+ _instance_settings_env: str | None = None
48
+
49
+ _auto_connect_path: Path = settings_dir / "auto_connect"
50
+ _private_django_api_path: Path = settings_dir / "private_django_api"
51
+
52
+ _cache_dir: Path | None = None
53
+
54
+ @property
55
+ def _instance_settings_path(self) -> Path:
56
+ return current_instance_settings_file()
57
+
58
+ @property
59
+ def settings_dir(self) -> Path:
60
+ """The directory that holds locally persisted settings."""
61
+ return settings_dir
62
+
63
+ @property
64
+ def auto_connect(self) -> bool:
65
+ """Auto-connect to current instance upon `import lamindb`.
66
+
67
+ Upon installing `lamindb`, this setting is `False`.
68
+
69
+ Upon calling `lamin init` or `lamin connect` on the CLI, this setting is switched to `True`.
70
+
71
+ `ln.connect()` doesn't change the value of this setting.
72
+
73
+ You can manually change this setting
74
+
75
+ - in Python: `ln.setup.settings.auto_connect = True/False`
76
+ - via the CLI: `lamin settings set auto-connect true/false`
77
+ """
78
+ return self._auto_connect_path.exists()
79
+
80
+ @auto_connect.setter
81
+ def auto_connect(self, value: bool) -> None:
82
+ if value:
83
+ self._auto_connect_path.touch()
84
+ else:
85
+ self._auto_connect_path.unlink(missing_ok=True)
86
+
87
+ @property
88
+ def private_django_api(self) -> bool:
89
+ """Turn internal Django API private to clean up the API (default `False`).
90
+
91
+ This patches your local pip-installed django installation. You can undo
92
+ the patch by setting this back to `False`.
93
+ """
94
+ return self._private_django_api_path.exists()
95
+
96
+ @private_django_api.setter
97
+ def private_django_api(self, value: bool) -> None:
98
+ from ._private_django_api import private_django_api
99
+
100
+ # we don't want to call private_django_api() twice
101
+ if value and not self.private_django_api:
102
+ private_django_api()
103
+ self._private_django_api_path.touch()
104
+ elif not value and self.private_django_api:
105
+ private_django_api(reverse=True)
106
+ self._private_django_api_path.unlink(missing_ok=True)
107
+
108
+ @property
109
+ def user(self) -> UserSettings:
110
+ """Settings of current user."""
111
+ env_changed = (
112
+ self._user_settings_env is not None
113
+ and self._user_settings_env != get_env_name()
114
+ )
115
+ if self._user_settings is None or env_changed:
116
+ self._user_settings = load_or_create_user_settings()
117
+ self._user_settings_env = get_env_name()
118
+ if self._user_settings and self._user_settings.uid is None:
119
+ raise RuntimeError("Need to login, first: lamin login <email>")
120
+ return self._user_settings # type: ignore
121
+
122
+ @property
123
+ def instance(self) -> InstanceSettings:
124
+ """Settings of current LaminDB instance."""
125
+ env_changed = (
126
+ self._instance_settings_env is not None
127
+ and self._instance_settings_env != get_env_name()
128
+ )
129
+ if self._instance_settings is None or env_changed:
130
+ self._instance_settings = load_instance_settings()
131
+ self._instance_settings_env = get_env_name()
132
+ return self._instance_settings # type: ignore
133
+
134
+ @property
135
+ def storage(self) -> StorageSettings:
136
+ """Settings of default storage."""
137
+ return self.instance.storage
138
+
139
+ @property
140
+ def _instance_exists(self):
141
+ try:
142
+ self.instance # noqa
143
+ return True
144
+ # this is implicit logic that catches if no instance is loaded
145
+ except SystemExit:
146
+ return False
147
+
148
+ @property
149
+ def cache_dir(self) -> UPath:
150
+ """Cache root, a local directory to cache cloud files."""
151
+ if "LAMIN_CACHE_DIR" in os.environ:
152
+ cache_dir = UPath(os.environ["LAMIN_CACHE_DIR"])
153
+ elif self._cache_dir is None:
154
+ cache_path = load_system_storage_settings().get("lamindb_cache_path", None)
155
+ cache_dir = _process_cache_path(cache_path)
156
+ if cache_dir is None:
157
+ cache_dir = DEFAULT_CACHE_DIR
158
+ self._cache_dir = cache_dir
159
+ else:
160
+ cache_dir = self._cache_dir
161
+ cache_dir.mkdir(parents=True, exist_ok=True)
162
+ return cache_dir
163
+
164
+ def __repr__(self) -> str:
165
+ """Rich string representation."""
166
+ repr = self.user.__repr__()
167
+ repr += f"\nAuto-connect in Python: {self.auto_connect}\n"
168
+ repr += f"Private Django API: {self.private_django_api}\n"
169
+ repr += f"Cache directory: {self.cache_dir}\n"
170
+ if self._instance_exists:
171
+ repr += self.instance.__repr__()
172
+ else:
173
+ repr += "\nNo instance connected"
174
+ return repr
175
+
176
+
177
+ def get_env_name():
178
+ if "LAMIN_ENV" in os.environ:
179
+ return os.environ["LAMIN_ENV"]
180
+ else:
181
+ return "prod"
182
+
183
+
184
+ settings = SetupSettings()