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,151 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+
6
+ from upath.implementations.cloud import S3Path
7
+
8
+ HOSTED_REGIONS = [
9
+ "eu-central-1",
10
+ "eu-west-2",
11
+ "us-east-1",
12
+ "us-east-2",
13
+ "us-west-1",
14
+ "us-west-2",
15
+ ]
16
+ lamin_env = os.getenv("LAMIN_ENV")
17
+ if lamin_env is None or lamin_env == "prod":
18
+ hosted_buckets_list = [f"s3://lamin-{region}" for region in HOSTED_REGIONS]
19
+ hosted_buckets_list.append("s3://scverse-spatial-eu-central-1")
20
+ HOSTED_BUCKETS = tuple(hosted_buckets_list)
21
+ else:
22
+ HOSTED_BUCKETS = ("s3://lamin-hosted-test",) # type: ignore
23
+
24
+
25
+ def _keep_trailing_slash(path_str: str):
26
+ return path_str if path_str[-1] == "/" else path_str + "/"
27
+
28
+
29
+ AWS_CREDENTIALS_EXPIRATION = 11 * 60 * 60 # refresh credentials after 11 hours
30
+
31
+
32
+ class AWSCredentialsManager:
33
+ def __init__(self):
34
+ self._credentials_cache = {}
35
+
36
+ from s3fs import S3FileSystem
37
+
38
+ # this is cached so will be resued with the connection initialized
39
+ fs = S3FileSystem(cache_regions=True)
40
+ fs.connect()
41
+ self.anon = fs.session._credentials is None
42
+
43
+ def _find_root(self, path_str: str) -> str | None:
44
+ roots = self._credentials_cache.keys()
45
+ if path_str in roots:
46
+ return path_str
47
+ roots = sorted(roots, key=len, reverse=True)
48
+ for root in roots:
49
+ if path_str.startswith(root):
50
+ return root
51
+ return None
52
+
53
+ def _is_active(self, root: str) -> bool:
54
+ return (
55
+ time.time() - self._credentials_cache[root]["time"]
56
+ ) < AWS_CREDENTIALS_EXPIRATION
57
+
58
+ def _set_cached_credentials(self, root: str, credentials: dict):
59
+ if root not in self._credentials_cache:
60
+ self._credentials_cache[root] = {}
61
+ self._credentials_cache[root]["credentials"] = credentials
62
+ self._credentials_cache[root]["time"] = time.time()
63
+
64
+ def _get_cached_credentials(self, root: str) -> dict:
65
+ return self._credentials_cache[root]["credentials"]
66
+
67
+ def _path_inject_options(self, path: S3Path, credentials: dict) -> S3Path:
68
+ if credentials == {}:
69
+ # credentials were specified manually for the path
70
+ if "anon" in path.storage_options:
71
+ anon = path.storage_options["anon"]
72
+ elif path.fs.key is not None and path.fs.secret is not None:
73
+ anon = False
74
+ else:
75
+ anon = self.anon
76
+ connection_options = {"anon": anon}
77
+ else:
78
+ connection_options = credentials
79
+
80
+ if "cache_regions" in path.storage_options:
81
+ cache_regions = path.storage_options["cache_regions"]
82
+ else:
83
+ cache_regions = True
84
+
85
+ return S3Path(path, cache_regions=cache_regions, **connection_options)
86
+
87
+ def enrich_path(self, path: S3Path, access_token: str | None = None) -> S3Path:
88
+ # trailing slash is needed to avoid returning incorrect results
89
+ # with .startswith
90
+ # for example s3://lamindata-eu should not receive cache for s3://lamindata
91
+ path_str = _keep_trailing_slash(path.as_posix())
92
+ root = self._find_root(path_str)
93
+
94
+ if root is not None:
95
+ set_cache = False
96
+ credentials = self._get_cached_credentials(root)
97
+
98
+ if access_token is not None:
99
+ set_cache = True
100
+ elif credentials != {}:
101
+ # update credentials
102
+ if not self._is_active(root):
103
+ set_cache = True
104
+ else:
105
+ set_cache = True
106
+
107
+ if set_cache:
108
+ from ._hub_core import access_aws
109
+ from ._settings import settings
110
+
111
+ if settings.user.handle != "anonymous" or access_token is not None:
112
+ storage_root_info = access_aws(path_str, access_token=access_token)
113
+ else:
114
+ storage_root_info = {"credentials": {}, "accessibility": {}}
115
+
116
+ accessibility = storage_root_info["accessibility"]
117
+ is_managed = accessibility.get("is_managed", False)
118
+ if is_managed:
119
+ credentials = storage_root_info["credentials"]
120
+ else:
121
+ credentials = {}
122
+
123
+ if access_token is None:
124
+ if "storage_root" in accessibility:
125
+ root = accessibility["storage_root"]
126
+ # just to be safe
127
+ root = None if root == "" else root
128
+ if root is None:
129
+ # heuristic
130
+ # do not write the first level for the known hosted buckets
131
+ if path_str.startswith(HOSTED_BUCKETS):
132
+ root = "/".join(path.path.rstrip("/").split("/")[:2])
133
+ else:
134
+ # write the bucket for everything else
135
+ root = path._url.netloc
136
+ root = "s3://" + root
137
+ self._set_cached_credentials(_keep_trailing_slash(root), credentials)
138
+
139
+ return self._path_inject_options(path, credentials)
140
+
141
+
142
+ _aws_credentials_manager: AWSCredentialsManager | None = None
143
+
144
+
145
+ def get_aws_credentials_manager() -> AWSCredentialsManager:
146
+ global _aws_credentials_manager
147
+
148
+ if _aws_credentials_manager is None:
149
+ _aws_credentials_manager = AWSCredentialsManager()
150
+
151
+ return _aws_credentials_manager
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def get_location(ip="ipinfo.io"):
5
+ import requests # type: ignore
6
+
7
+ response = requests.get(f"http://{ip}/json").json()
8
+ loc = response["loc"].split(",")
9
+ return {"latitude": float(loc[0]), "longitude": float(loc[1])}
10
+
11
+
12
+ def haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
13
+ import math
14
+
15
+ R = 6371 # Radius of the Earth in kilometers
16
+ dLat = math.radians(lat2 - lat1)
17
+ dLon = math.radians(lon2 - lon1)
18
+ a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(
19
+ math.radians(lat1)
20
+ ) * math.cos(math.radians(lat2)) * math.sin(dLon / 2) * math.sin(dLon / 2)
21
+ c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
22
+ distance = R * c
23
+ return distance
24
+
25
+
26
+ def find_closest_aws_region() -> str:
27
+ aws_region_locations = {
28
+ "us-east-1": {"latitude": 39.0, "longitude": -77.0}, # Northern Virginia
29
+ "us-east-2": {"latitude": 40.0, "longitude": -83.0}, # Ohio
30
+ "us-west-1": {"latitude": 37.77, "longitude": -122.41}, # Northern California
31
+ "us-west-2": {"latitude": 45.52, "longitude": -122.68}, # Oregon
32
+ "eu-central-1": {"latitude": 50.11, "longitude": 8.68}, # Frankfurt
33
+ "eu-west-2": {"latitude": 51.51, "longitude": -0.13}, # London, UK
34
+ }
35
+ your_location = get_location()
36
+ closest_region = ""
37
+ min_distance = float("inf")
38
+ for region in aws_region_locations:
39
+ region_location = aws_region_locations[region]
40
+ distance = haversine(
41
+ your_location["latitude"],
42
+ your_location["longitude"],
43
+ region_location["latitude"],
44
+ region_location["longitude"],
45
+ )
46
+ if distance < min_distance:
47
+ closest_region, min_distance = region, distance
48
+ return closest_region
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ # BSD 3-Clause License
4
+ # Copyright (c) 2017-2018 P. Angerer, F. Alexander Wolf, Theis Lab
5
+ # All rights reserved.
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ # * Redistributions of source code must retain the above copyright notice, this
9
+ # list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # * Neither the name of the copyright holder nor the names of its
14
+ # contributors may be used to endorse or promote products derived from
15
+ # this software without specific prior written permission.
16
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
+ import warnings
27
+ from functools import wraps
28
+
29
+
30
+ def deprecated(new_name: str):
31
+ """Deprecated.
32
+
33
+ This is a decorator which can be used to mark functions
34
+ as deprecated. It will result in a warning being emitted
35
+ when the function is used.
36
+ """
37
+
38
+ def decorator(func):
39
+ @wraps(func)
40
+ def new_func(*args, **kwargs):
41
+ # turn off filter
42
+ warnings.simplefilter("always", DeprecationWarning)
43
+ warnings.warn(
44
+ f"Use {new_name} instead of {func.__name__}, "
45
+ f"{func.__name__} will be removed in the future.",
46
+ category=DeprecationWarning,
47
+ stacklevel=2,
48
+ )
49
+ warnings.simplefilter("default", DeprecationWarning) # reset filter
50
+ return func(*args, **kwargs)
51
+
52
+ setattr(new_func, "__deprecated", True)
53
+ return new_func
54
+
55
+ return decorator
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from textwrap import dedent
4
+
5
+
6
+ def doc_args(*args):
7
+ """Pass arguments to docstrings."""
8
+
9
+ def dec(obj):
10
+ obj.__orig_doc__ = obj.__doc__
11
+ obj.__doc__ = dedent(obj.__doc__).format(*args)
12
+ return obj
13
+
14
+ return dec
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from urllib.request import urlretrieve
6
+
7
+ from gotrue.errors import AuthUnknownError
8
+ from lamin_utils import logger
9
+ from pydantic_settings import BaseSettings
10
+ from supabase import Client, create_client # type: ignore
11
+ from supabase.lib.client_options import ClientOptions
12
+
13
+
14
+ class Connector(BaseSettings):
15
+ url: str
16
+ key: str
17
+
18
+
19
+ def load_fallback_connector() -> Connector:
20
+ url = "https://lamin-site-assets.s3.amazonaws.com/connector.env"
21
+ connector_file, _ = urlretrieve(url)
22
+ connector = Connector(_env_file=connector_file)
23
+ return connector
24
+
25
+
26
+ PROD_URL = "https://hub.lamin.ai"
27
+ PROD_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxhZXNhdW1tZHlkbGxwcGdmY2h1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTY4NDA1NTEsImV4cCI6MTk3MjQxNjU1MX0.WUeCRiun0ExUxKIv5-CtjF6878H8u26t0JmCWx3_2-c"
28
+
29
+
30
+ class Environment:
31
+ def __init__(self, fallback: bool = False):
32
+ lamin_env = os.getenv("LAMIN_ENV")
33
+ if lamin_env is None:
34
+ lamin_env = "prod"
35
+ # set public key
36
+ if lamin_env == "prod":
37
+ if not fallback:
38
+ url = PROD_URL
39
+ key = PROD_ANON_KEY
40
+ else:
41
+ connector = load_fallback_connector()
42
+ url = connector.url
43
+ key = connector.key
44
+ elif lamin_env == "staging":
45
+ url = "https://amvrvdwndlqdzgedrqdv.supabase.co"
46
+ key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFtdnJ2ZHduZGxxZHpnZWRycWR2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NzcxNTcxMzMsImV4cCI6MTk5MjczMzEzM30.Gelt3dQEi8tT4j-JA36RbaZuUvxRnczvRr3iyRtzjY0"
47
+ elif lamin_env == "staging-test":
48
+ url = "https://iugyyajllqftbpidapak.supabase.co"
49
+ key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Iml1Z3l5YWpsbHFmdGJwaWRhcGFrIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTQyMjYyODMsImV4cCI6MjAwOTgwMjI4M30.s7B0gMogFhUatMSwlfuPJ95kWhdCZMn1ROhZ3t6Og90"
50
+ elif lamin_env == "prod-test":
51
+ url = "https://xtdacpwiqwpbxsatoyrv.supabase.co"
52
+ key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0ZGFjcHdpcXdwYnhzYXRveXJ2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTQyMjYxNDIsImV4cCI6MjAwOTgwMjE0Mn0.Dbi27qujTt8Ei9gfp9KnEWTYptE5KUbZzEK6boL46k4"
53
+ else:
54
+ url = os.environ["SUPABASE_API_URL"]
55
+ key = os.environ["SUPABASE_ANON_KEY"]
56
+ self.lamin_env: str = lamin_env
57
+ self.supabase_api_url: str = url
58
+ self.supabase_anon_key: str = key
59
+
60
+
61
+ # runs ~0.5s
62
+ def connect_hub(
63
+ fallback_env: bool = False, client_options: ClientOptions | None = None
64
+ ) -> Client:
65
+ env = Environment(fallback=fallback_env)
66
+ if client_options is None:
67
+ client_options = ClientOptions(auto_refresh_token=False)
68
+ return create_client(env.supabase_api_url, env.supabase_anon_key, client_options)
69
+
70
+
71
+ def connect_hub_with_auth(
72
+ fallback_env: bool = False,
73
+ renew_token: bool = False,
74
+ access_token: str | None = None,
75
+ ) -> Client:
76
+ hub = connect_hub(fallback_env=fallback_env)
77
+ if access_token is None:
78
+ from lamindb_setup import settings
79
+
80
+ if renew_token:
81
+ settings.user.access_token = get_access_token(
82
+ settings.user.email, settings.user.password, settings.user.api_key
83
+ )
84
+ access_token = settings.user.access_token
85
+ hub.postgrest.auth(access_token)
86
+ hub.functions.set_auth(access_token)
87
+ return hub
88
+
89
+
90
+ # runs ~0.5s
91
+ def get_access_token(
92
+ email: str | None = None, password: str | None = None, api_key: str | None = None
93
+ ):
94
+ hub = connect_hub()
95
+ try:
96
+ if api_key is not None:
97
+ auth_response = hub.functions.invoke(
98
+ "get-jwt-v1",
99
+ invoke_options={"body": {"api_key": api_key}},
100
+ )
101
+ return json.loads(auth_response)["accessToken"]
102
+ auth_response = hub.auth.sign_in_with_password(
103
+ {
104
+ "email": email,
105
+ "password": password,
106
+ }
107
+ )
108
+ return auth_response.session.access_token
109
+ finally:
110
+ hub.auth.sign_out(options={"scope": "local"})
111
+
112
+
113
+ def call_with_fallback_auth(
114
+ callable,
115
+ **kwargs,
116
+ ):
117
+ access_token = kwargs.pop("access_token", None)
118
+
119
+ if access_token is not None:
120
+ try:
121
+ client = connect_hub_with_auth(access_token=access_token)
122
+ result = callable(**kwargs, client=client)
123
+ finally:
124
+ try:
125
+ client.auth.sign_out(options={"scope": "local"})
126
+ except NameError:
127
+ pass
128
+ return result
129
+
130
+ for renew_token, fallback_env in [(False, False), (True, False), (False, True)]:
131
+ try:
132
+ if renew_token:
133
+ logger.warning(
134
+ "renewing expired lamin token: call `lamin login <your-handle>` to avoid this"
135
+ )
136
+ client = connect_hub_with_auth(
137
+ renew_token=renew_token, fallback_env=fallback_env
138
+ )
139
+ result = callable(**kwargs, client=client)
140
+ break
141
+ # we use Exception here as the ways in which the client fails upon 401
142
+ # are not consistent and keep changing
143
+ # because we ultimately raise the error, it's OK I'd say
144
+ except Exception as e:
145
+ if fallback_env:
146
+ raise e
147
+ finally:
148
+ try:
149
+ client.auth.sign_out(options={"scope": "local"})
150
+ except NameError:
151
+ pass
152
+ return result
153
+
154
+
155
+ def call_with_fallback(
156
+ callable,
157
+ **kwargs,
158
+ ):
159
+ for fallback_env in [False, True]:
160
+ try:
161
+ client = connect_hub(fallback_env=fallback_env)
162
+ result = callable(**kwargs, client=client)
163
+ break
164
+ except AuthUnknownError as e:
165
+ if fallback_env:
166
+ raise e
167
+ finally:
168
+ try:
169
+ # in case there was sign in
170
+ client.auth.sign_out(options={"scope": "local"})
171
+ except NameError:
172
+ pass
173
+ return result