redivis 0.22.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.
- redivis/__init__.py +194 -0
- redivis/_version.py +1 -0
- redivis/classes/Base.py +22 -0
- redivis/classes/Dataset.py +301 -0
- redivis/classes/Datasource.py +178 -0
- redivis/classes/Directory.py +304 -0
- redivis/classes/Export.py +159 -0
- redivis/classes/File.py +375 -0
- redivis/classes/Member.py +88 -0
- redivis/classes/Notebook.py +226 -0
- redivis/classes/Organization.py +89 -0
- redivis/classes/Parameter.py +85 -0
- redivis/classes/Query.py +144 -0
- redivis/classes/ReadStream.py +18 -0
- redivis/classes/Secret.py +22 -0
- redivis/classes/Table.py +417 -0
- redivis/classes/Transform.py +173 -0
- redivis/classes/Upload.py +332 -0
- redivis/classes/User.py +63 -0
- redivis/classes/Variable.py +92 -0
- redivis/classes/Version.py +107 -0
- redivis/classes/Workflow.py +194 -0
- redivis/classes/__init__.py +0 -0
- redivis/common/TabularReader.py +1038 -0
- redivis/common/__init__.py +0 -0
- redivis/common/api_request.py +286 -0
- redivis/common/auth.py +347 -0
- redivis/common/config.py +171 -0
- redivis/common/exceptions.py +114 -0
- redivis/common/fetch_rows.py +1066 -0
- redivis/common/fsspec.py +142 -0
- redivis/common/mount_directory.py +896 -0
- redivis/common/retryable_download.py +885 -0
- redivis/common/retryable_upload.py +199 -0
- redivis/common/util.py +336 -0
- redivis-0.22.0.dist-info/METADATA +80 -0
- redivis-0.22.0.dist-info/RECORD +40 -0
- redivis-0.22.0.dist-info/WHEEL +5 -0
- redivis-0.22.0.dist-info/licenses/LICENSE +21 -0
- redivis-0.22.0.dist-info/top_level.txt +1 -0
redivis/__init__.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from .classes.Workflow import Workflow as workflow
|
|
2
|
+
from .classes.Dataset import Dataset as dataset
|
|
3
|
+
from .classes.Datasource import Datasource as datasource
|
|
4
|
+
from .classes.User import User as user
|
|
5
|
+
from .classes.Organization import Organization as organization
|
|
6
|
+
from .classes.Parameter import Parameter as parameter
|
|
7
|
+
from .classes.Query import Query as query
|
|
8
|
+
from .classes.Table import Table as table
|
|
9
|
+
from .classes.Notebook import Notebook as notebook
|
|
10
|
+
from .classes.Transform import Transform as transform
|
|
11
|
+
from .common import exceptions
|
|
12
|
+
from .common.api_request import make_request as make_api_request
|
|
13
|
+
from .common.config import config, get_config as _get_config
|
|
14
|
+
|
|
15
|
+
# Note: these should be deleted at the end to clean up the namespace
|
|
16
|
+
import warnings
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
from ._version import __version__
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def file(*args, **kwargs):
|
|
23
|
+
|
|
24
|
+
raise exceptions.DeprecationError(
|
|
25
|
+
'Calling redivis.file() is no longer supported. Please use redivis.table("table_reference").file("filename") instead.'
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _warning(message, category, filename, lineno, file=None, line=None):
|
|
30
|
+
print(f"Warning: {message}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
warnings.showwarning = _warning
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def authenticate(scope=None, force_reauthentication=False):
|
|
37
|
+
from .common.auth import clear_cached_credentials, get_auth_token
|
|
38
|
+
|
|
39
|
+
if force_reauthentication:
|
|
40
|
+
clear_cached_credentials()
|
|
41
|
+
|
|
42
|
+
if isinstance(scope, str):
|
|
43
|
+
scope = [scope]
|
|
44
|
+
|
|
45
|
+
get_auth_token(scope=scope)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def current_notebook():
|
|
49
|
+
import os
|
|
50
|
+
|
|
51
|
+
if os.getenv("REDIVIS_DEFAULT_NOTEBOOK") is not None:
|
|
52
|
+
return notebook(os.getenv("REDIVIS_DEFAULT_NOTEBOOK"))
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def current_user():
|
|
58
|
+
res = make_api_request(method="GET", path="/users/me")
|
|
59
|
+
return user(res["name"], properties=res)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def current_workflow():
|
|
63
|
+
if _get_config("default_workflow") is not None:
|
|
64
|
+
return workflow(_get_config("default_workflow"))
|
|
65
|
+
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"workflow",
|
|
71
|
+
"dataset",
|
|
72
|
+
"datasource",
|
|
73
|
+
"user",
|
|
74
|
+
"organization",
|
|
75
|
+
"parameter",
|
|
76
|
+
"query",
|
|
77
|
+
"table",
|
|
78
|
+
"notebook",
|
|
79
|
+
"transform",
|
|
80
|
+
"exceptions",
|
|
81
|
+
"make_api_request",
|
|
82
|
+
"__version__",
|
|
83
|
+
"authenticate",
|
|
84
|
+
"current_notebook",
|
|
85
|
+
"current_user",
|
|
86
|
+
"current_workflow",
|
|
87
|
+
"config",
|
|
88
|
+
"file",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _install_excepthook():
|
|
93
|
+
import sys
|
|
94
|
+
import traceback
|
|
95
|
+
import os
|
|
96
|
+
|
|
97
|
+
_package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
98
|
+
_original_hook = sys.excepthook
|
|
99
|
+
|
|
100
|
+
def _format_filtered_tb(exc_type, exc_value, exc_tb):
|
|
101
|
+
|
|
102
|
+
entries = traceback.extract_tb(exc_tb)
|
|
103
|
+
|
|
104
|
+
# Extract all frames, keep only those outside the redivis package
|
|
105
|
+
# Also exclude ipython noise
|
|
106
|
+
# Directories to filter out of tracebacks
|
|
107
|
+
_filter_dirs = [
|
|
108
|
+
_package_dir,
|
|
109
|
+
os.path.join("IPython", ""),
|
|
110
|
+
os.path.join("ipykernel", ""),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
user_entries = [
|
|
114
|
+
e
|
|
115
|
+
for e in entries
|
|
116
|
+
if not any(d in os.path.abspath(e.filename) for d in _filter_dirs)
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
lines = []
|
|
120
|
+
lines.append("Traceback (most recent call last):\n")
|
|
121
|
+
if user_entries:
|
|
122
|
+
lines.extend(traceback.format_list(user_entries))
|
|
123
|
+
else:
|
|
124
|
+
# No user frames; show the last frame before filtering so there's some context
|
|
125
|
+
last_entry = entries[-1] if entries else None
|
|
126
|
+
if last_entry:
|
|
127
|
+
lines.extend(traceback.format_list([last_entry]))
|
|
128
|
+
else:
|
|
129
|
+
lines.append(" (full traceback omitted from redivis internals)\n")
|
|
130
|
+
|
|
131
|
+
lines.append(f"{exc_type.__name__}: {exc_value}\n")
|
|
132
|
+
return "".join(lines)
|
|
133
|
+
|
|
134
|
+
def _custom_excepthook(exc_type, exc_value, exc_tb):
|
|
135
|
+
if isinstance(exc_value, exceptions.RedivisError):
|
|
136
|
+
print(
|
|
137
|
+
_format_filtered_tb(exc_type, exc_value, exc_tb),
|
|
138
|
+
file=sys.stderr,
|
|
139
|
+
end="",
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
_original_hook(exc_type, exc_value, exc_tb)
|
|
143
|
+
|
|
144
|
+
sys.excepthook = _custom_excepthook
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
from IPython import get_ipython
|
|
148
|
+
|
|
149
|
+
ipython = get_ipython()
|
|
150
|
+
if ipython is not None:
|
|
151
|
+
|
|
152
|
+
def _ipython_custom_exc(shell, exc_type, exc_value, exc_tb, tb_offset=None):
|
|
153
|
+
try:
|
|
154
|
+
print(
|
|
155
|
+
_format_filtered_tb(exc_type, exc_value, exc_tb),
|
|
156
|
+
file=sys.stderr,
|
|
157
|
+
end="",
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(
|
|
161
|
+
"An error occurred while formatting the exception:",
|
|
162
|
+
e,
|
|
163
|
+
file=sys.stderr,
|
|
164
|
+
)
|
|
165
|
+
# Fallback to original IPython exception handler
|
|
166
|
+
shell.showtraceback(
|
|
167
|
+
(exc_type, exc_value, exc_tb), tb_offset=tb_offset
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
ipython.set_custom_exc((exceptions.RedivisError,), _ipython_custom_exc)
|
|
171
|
+
except ImportError:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if os.getenv("REDIVIS_INSTALL_EXCEPTHOOK", "1").lower() not in ("0", "false", "no"):
|
|
176
|
+
_install_excepthook()
|
|
177
|
+
|
|
178
|
+
# Register fsspec filesystem if available
|
|
179
|
+
try:
|
|
180
|
+
from .common.fsspec import register
|
|
181
|
+
|
|
182
|
+
register()
|
|
183
|
+
except (ImportError, ModuleNotFoundError):
|
|
184
|
+
# fsspec or its integration is not installed; registration is optional
|
|
185
|
+
pass
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
# Unexpected error during registration; warn rather than fail silently
|
|
188
|
+
warnings.warn(
|
|
189
|
+
f"Failed to register Redivis fsspec filesystem due to an unexpected error: {exc}",
|
|
190
|
+
RuntimeWarning,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# clean up namespace
|
|
194
|
+
del os, warnings
|
redivis/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.22.0"
|
redivis/classes/Base.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class Base:
|
|
2
|
+
def __getitem__(self, key):
|
|
3
|
+
return (
|
|
4
|
+
self.properties[key]
|
|
5
|
+
if hasattr(self, "properties")
|
|
6
|
+
and self.properties is not None
|
|
7
|
+
and key in self.properties
|
|
8
|
+
else None
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
def __repr__(self) -> str:
|
|
12
|
+
field_strings = []
|
|
13
|
+
for key, field in vars(self).items():
|
|
14
|
+
if (
|
|
15
|
+
key != "properties"
|
|
16
|
+
and key != "scoped_reference"
|
|
17
|
+
and field is not None
|
|
18
|
+
and not isinstance(field, Base)
|
|
19
|
+
):
|
|
20
|
+
field_strings.append(f"{key}:{field!r}")
|
|
21
|
+
|
|
22
|
+
return f"<{self.__class__.__name__} {' '.join(field_strings)}>"
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from .Table import Table
|
|
2
|
+
from .Version import Version
|
|
3
|
+
import os
|
|
4
|
+
import warnings
|
|
5
|
+
from .Query import Query
|
|
6
|
+
from .Base import Base
|
|
7
|
+
from urllib.parse import quote as quote_uri
|
|
8
|
+
from ..common.util import split_reference
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from ..common import exceptions
|
|
12
|
+
from ..common.api_request import make_request, make_paginated_request
|
|
13
|
+
from ..common.config import get_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Dataset(Base):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
name,
|
|
20
|
+
*,
|
|
21
|
+
version=None,
|
|
22
|
+
user=None,
|
|
23
|
+
organization=None,
|
|
24
|
+
properties=None,
|
|
25
|
+
):
|
|
26
|
+
if not user and not organization:
|
|
27
|
+
from .User import User
|
|
28
|
+
from .Organization import Organization
|
|
29
|
+
|
|
30
|
+
if len(split_reference(name)) == 2:
|
|
31
|
+
username, name = split_reference(name)
|
|
32
|
+
organization = Organization(username)
|
|
33
|
+
user = User(username)
|
|
34
|
+
elif get_config("default_user"):
|
|
35
|
+
user = User(get_config("default_user"))
|
|
36
|
+
elif get_config("default_organization"):
|
|
37
|
+
organization = Organization(get_config("default_organization"))
|
|
38
|
+
else:
|
|
39
|
+
raise exceptions.ValueError(
|
|
40
|
+
"Invalid dataset specifier, must be the fully qualified reference if no owner is specified"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.name = name.split(":")[0]
|
|
44
|
+
self.user = user
|
|
45
|
+
self.organization = organization
|
|
46
|
+
reference_id = ""
|
|
47
|
+
reference_id_split = re.sub(
|
|
48
|
+
r":(v\d+[._]\d+|current|next|sample)", "", name
|
|
49
|
+
).split(":")
|
|
50
|
+
if len(reference_id_split) > 1:
|
|
51
|
+
reference_id = reference_id_split[1]
|
|
52
|
+
|
|
53
|
+
if reference_id:
|
|
54
|
+
reference_id = f":{reference_id}"
|
|
55
|
+
|
|
56
|
+
if not version:
|
|
57
|
+
version_search = re.search(r":(v\d+[._]\d+|current|next)", name)
|
|
58
|
+
if version_search:
|
|
59
|
+
version = version_search[1]
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
version
|
|
63
|
+
and version != "current"
|
|
64
|
+
and version != "next"
|
|
65
|
+
and not version.lower().startswith("v")
|
|
66
|
+
):
|
|
67
|
+
version = f"v{version}"
|
|
68
|
+
|
|
69
|
+
self.version_tag = version
|
|
70
|
+
self.scoped_reference = (properties or {}).get(
|
|
71
|
+
"scopedReference",
|
|
72
|
+
f"{self.name}{reference_id}{f':{version}' if version else ''}",
|
|
73
|
+
)
|
|
74
|
+
self.qualified_reference = (properties or {}).get(
|
|
75
|
+
"qualifiedReference",
|
|
76
|
+
f"{(self.organization or self.user).name}.{self.scoped_reference}",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self.uri = f"/datasets/{quote_uri(self.qualified_reference, '')}"
|
|
80
|
+
self.properties = properties
|
|
81
|
+
|
|
82
|
+
def __repr__(self):
|
|
83
|
+
return f"<Dataset {self.qualified_reference}>"
|
|
84
|
+
|
|
85
|
+
def _rectify_ambiguous_owner(self):
|
|
86
|
+
if self.user and self.organization:
|
|
87
|
+
if (self.properties or {}).get("owner"):
|
|
88
|
+
if self.properties.get("owner")["kind"] == "user":
|
|
89
|
+
self.organization = None
|
|
90
|
+
else:
|
|
91
|
+
self.user = None
|
|
92
|
+
elif self.organization.exists():
|
|
93
|
+
self.user = None
|
|
94
|
+
else:
|
|
95
|
+
self.organization = None
|
|
96
|
+
|
|
97
|
+
def create(self, *, public_access_level="none", description=None):
|
|
98
|
+
self._rectify_ambiguous_owner()
|
|
99
|
+
if self.organization:
|
|
100
|
+
path = f"{self.organization.uri}/datasets"
|
|
101
|
+
else:
|
|
102
|
+
path = f"{self.user.uri}/datasets"
|
|
103
|
+
|
|
104
|
+
properties = make_request(
|
|
105
|
+
method="POST",
|
|
106
|
+
path=path,
|
|
107
|
+
payload={
|
|
108
|
+
"name": self.name,
|
|
109
|
+
"publicAccessLevel": public_access_level,
|
|
110
|
+
"description": description,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
update_properties(self, properties)
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def create_next_version(self, *, if_not_exists=False):
|
|
117
|
+
if not self.properties or "nextVersion" not in self.properties:
|
|
118
|
+
self.get()
|
|
119
|
+
|
|
120
|
+
if not self.properties["nextVersion"]:
|
|
121
|
+
make_request(method="POST", path=f"{self.uri}/versions")
|
|
122
|
+
elif not if_not_exists:
|
|
123
|
+
raise exceptions.ValueError(
|
|
124
|
+
f"Next version already exists at {self.properties['nextVersion']['datasetUri']}. To avoid this error, set argument if_not_exists to True"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
self._rectify_ambiguous_owner()
|
|
128
|
+
next_version_dataset = Dataset(
|
|
129
|
+
name=self.scoped_reference,
|
|
130
|
+
user=self.user,
|
|
131
|
+
organization=self.organization,
|
|
132
|
+
version="next",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return next_version_dataset.get()
|
|
136
|
+
|
|
137
|
+
def delete(self):
|
|
138
|
+
make_request(
|
|
139
|
+
method="DELETE",
|
|
140
|
+
path=self.uri,
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
def exists(self):
|
|
145
|
+
try:
|
|
146
|
+
make_request(method="HEAD", path=self.uri)
|
|
147
|
+
return True
|
|
148
|
+
except exceptions.NotFoundError:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
def get(self):
|
|
152
|
+
properties = make_request(method="GET", path=self.uri)
|
|
153
|
+
update_properties(self, properties)
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
def list_tables(self, max_results=None):
|
|
157
|
+
tables = make_paginated_request(
|
|
158
|
+
path=f"{self.uri}/tables", page_size=100, max_results=max_results
|
|
159
|
+
)
|
|
160
|
+
return [
|
|
161
|
+
Table(table["name"], dataset=self, properties=table) for table in tables
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
def list_versions(self, max_results=None):
|
|
165
|
+
versions = make_paginated_request(
|
|
166
|
+
path=f"{self.uri}/versions", page_size=100, max_results=max_results
|
|
167
|
+
)
|
|
168
|
+
return [
|
|
169
|
+
Version(version["tag"], dataset=self, properties=version)
|
|
170
|
+
for version in versions
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
def query(self, query):
|
|
174
|
+
return Query(query, default_dataset=self.qualified_reference)
|
|
175
|
+
|
|
176
|
+
def release(self, *, release_notes=None, wait_for_finalization=False):
|
|
177
|
+
version_res = make_request(
|
|
178
|
+
method="POST",
|
|
179
|
+
path=f"{self.uri}/versions/next/release",
|
|
180
|
+
payload={"releaseNotes": release_notes},
|
|
181
|
+
)
|
|
182
|
+
if wait_for_finalization:
|
|
183
|
+
Version(
|
|
184
|
+
version_res["tag"], dataset=self, properties=version_res
|
|
185
|
+
)._wait_for_finalization()
|
|
186
|
+
self.uri = version_res["datasetUri"]
|
|
187
|
+
self.get()
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
def unrelease(self, *, wait_for_finalization=False):
|
|
191
|
+
version_res = make_request(
|
|
192
|
+
method="POST",
|
|
193
|
+
path=f"{self.uri}/versions/current/unrelease",
|
|
194
|
+
)
|
|
195
|
+
if wait_for_finalization:
|
|
196
|
+
Version(
|
|
197
|
+
version_res["tag"], dataset=self, properties=version_res
|
|
198
|
+
)._wait_for_finalization()
|
|
199
|
+
self.uri = version_res["datasetUri"]
|
|
200
|
+
self.get()
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
def version(self, tag=None):
|
|
204
|
+
if not self.version_tag and not tag:
|
|
205
|
+
self.get()
|
|
206
|
+
|
|
207
|
+
tag = self.version_tag if tag is None else tag
|
|
208
|
+
return Version(tag, dataset=self)
|
|
209
|
+
|
|
210
|
+
def previous_version(self):
|
|
211
|
+
version = self.version().previous_version()
|
|
212
|
+
return version.dataset() if version else None
|
|
213
|
+
|
|
214
|
+
def next_version(self):
|
|
215
|
+
version = self.version().next_version()
|
|
216
|
+
return version.dataset() if version else None
|
|
217
|
+
|
|
218
|
+
def table(self, name):
|
|
219
|
+
return Table(name, dataset=self)
|
|
220
|
+
|
|
221
|
+
def add_labels(self, labels):
|
|
222
|
+
warnings.warn(
|
|
223
|
+
"This method is deprecated, and will be removed in a future version. Use dataset.update(labels_to_add=[...]) instead",
|
|
224
|
+
FutureWarning,
|
|
225
|
+
stacklevel=2,
|
|
226
|
+
)
|
|
227
|
+
return self.update(labels_to_add=labels)
|
|
228
|
+
|
|
229
|
+
def remove_labels(self, labels):
|
|
230
|
+
warnings.warn(
|
|
231
|
+
"This method is deprecated, and will be removed in a future version. Use dataset.update(labels_to_remove=[...]) instead",
|
|
232
|
+
FutureWarning,
|
|
233
|
+
stacklevel=2,
|
|
234
|
+
)
|
|
235
|
+
return self.update(labels_to_remove=labels)
|
|
236
|
+
|
|
237
|
+
def update(
|
|
238
|
+
self,
|
|
239
|
+
*,
|
|
240
|
+
name=None,
|
|
241
|
+
public_access_level=None,
|
|
242
|
+
description=None,
|
|
243
|
+
methodology_markdown=None,
|
|
244
|
+
usage_markdown=None,
|
|
245
|
+
labels=None,
|
|
246
|
+
labels_to_add=None,
|
|
247
|
+
labels_to_remove=None,
|
|
248
|
+
tags=None,
|
|
249
|
+
tags_to_add=None,
|
|
250
|
+
tags_to_remove=None,
|
|
251
|
+
):
|
|
252
|
+
payload = {}
|
|
253
|
+
if name:
|
|
254
|
+
payload["name"] = name
|
|
255
|
+
if public_access_level:
|
|
256
|
+
payload["publicAccessLevel"] = public_access_level
|
|
257
|
+
if description is not None:
|
|
258
|
+
payload["description"] = description
|
|
259
|
+
if methodology_markdown is not None:
|
|
260
|
+
payload["methodologyMarkdown"] = methodology_markdown
|
|
261
|
+
if usage_markdown is not None:
|
|
262
|
+
payload["usageMarkdown"] = usage_markdown
|
|
263
|
+
if labels is not None:
|
|
264
|
+
payload["labels"] = labels
|
|
265
|
+
if labels_to_add is not None:
|
|
266
|
+
payload["labelsToAdd"] = labels_to_add
|
|
267
|
+
if labels_to_remove is not None:
|
|
268
|
+
payload["labelsToRemove"] = labels_to_remove
|
|
269
|
+
if tags is not None:
|
|
270
|
+
payload["tags"] = tags
|
|
271
|
+
if tags_to_add is not None:
|
|
272
|
+
payload["tagsToAdd"] = tags_to_add
|
|
273
|
+
if tags_to_remove is not None:
|
|
274
|
+
payload["tagsToRemove"] = tags_to_remove
|
|
275
|
+
|
|
276
|
+
res = make_request(
|
|
277
|
+
method="PATCH",
|
|
278
|
+
path=self.uri,
|
|
279
|
+
payload=payload,
|
|
280
|
+
)
|
|
281
|
+
update_properties(self, res)
|
|
282
|
+
return self
|
|
283
|
+
|
|
284
|
+
def update_variables(self, variables):
|
|
285
|
+
make_request(
|
|
286
|
+
method="PATCH",
|
|
287
|
+
path=f"{self.uri}/variables",
|
|
288
|
+
payload={"variables": variables},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def update_properties(instance, properties):
|
|
293
|
+
instance.properties = properties
|
|
294
|
+
instance.qualified_reference = properties["qualifiedReference"]
|
|
295
|
+
instance.scoped_reference = properties["scopedReference"]
|
|
296
|
+
instance.name = properties["name"]
|
|
297
|
+
instance.uri = properties["uri"]
|
|
298
|
+
if (properties.get("version") or {}).get("tag"):
|
|
299
|
+
# In the form used in references (v1_0, rather than v1.0)
|
|
300
|
+
instance.version_tag = properties["version"]["tag"].replace(".", "_")
|
|
301
|
+
instance._rectify_ambiguous_owner()
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from .Table import Table
|
|
2
|
+
from .Base import Base
|
|
3
|
+
from ..common import exceptions
|
|
4
|
+
from ..common.api_request import make_request
|
|
5
|
+
|
|
6
|
+
from urllib.parse import quote as quote_uri
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from ..common.config import get_config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Datasource(Base):
|
|
13
|
+
def __init__(self, source, *, workflow=None, properties=None):
|
|
14
|
+
from .Workflow import Workflow
|
|
15
|
+
from .Dataset import Dataset
|
|
16
|
+
|
|
17
|
+
if not workflow:
|
|
18
|
+
if get_config("default_workflow"):
|
|
19
|
+
workflow = Workflow(get_config("default_workflow"))
|
|
20
|
+
else:
|
|
21
|
+
raise exceptions.ValueError(
|
|
22
|
+
"A workflow must be specified in the Datasource constructor, or set as the default via redivis.config(default_workflow=...) or the REDIVIS_DEFAULT_WORKFLOW environment variable"
|
|
23
|
+
)
|
|
24
|
+
if isinstance(source, Dataset) or isinstance(source, Workflow):
|
|
25
|
+
source = source.qualified_reference
|
|
26
|
+
|
|
27
|
+
self.workflow = workflow
|
|
28
|
+
self.source_reference = source
|
|
29
|
+
self.uri = (properties or {}).get(
|
|
30
|
+
"uri", f"{workflow.uri}/dataSources/{quote_uri(source)}"
|
|
31
|
+
)
|
|
32
|
+
self.properties = properties
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return f"<Datasource {self.source_reference}>"
|
|
36
|
+
|
|
37
|
+
def get(self):
|
|
38
|
+
self.properties = make_request(
|
|
39
|
+
method="GET",
|
|
40
|
+
path=self.uri,
|
|
41
|
+
)
|
|
42
|
+
self.uri = self.properties["uri"]
|
|
43
|
+
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def create(self):
|
|
47
|
+
from .Dataset import Dataset
|
|
48
|
+
|
|
49
|
+
payload = {}
|
|
50
|
+
if Dataset(self.source_reference).exists():
|
|
51
|
+
payload["sourceDataset"] = self.source_reference
|
|
52
|
+
else:
|
|
53
|
+
payload["sourceWorkflow"] = self.source_reference
|
|
54
|
+
|
|
55
|
+
self.properties = make_request(
|
|
56
|
+
method="POST", path=f"{self.workflow.uri}/dataSources", payload=payload
|
|
57
|
+
)
|
|
58
|
+
self.uri = self.properties["uri"]
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def delete(self):
|
|
62
|
+
# Also deletes the transforms and notebooks that depend on the datasource
|
|
63
|
+
make_request(method="DELETE", path=self.uri)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
def exists(self):
|
|
67
|
+
try:
|
|
68
|
+
make_request(method="HEAD", path=self.uri)
|
|
69
|
+
return True
|
|
70
|
+
except exceptions.NotFoundError:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
def source_dataset(self):
|
|
74
|
+
from .Dataset import Dataset
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
source_dataset = (self.properties or {})["sourceDataset"]
|
|
78
|
+
except KeyError:
|
|
79
|
+
self.get()
|
|
80
|
+
if not "sourceDataset" in self.properties:
|
|
81
|
+
raise exceptions.ValueError(
|
|
82
|
+
"This datasource doesn't have a source dataset. Use the source_workflow() method instead."
|
|
83
|
+
)
|
|
84
|
+
source_dataset = self.properties["sourceDataset"]
|
|
85
|
+
return Dataset(source_dataset["qualifiedReference"], properties=source_dataset)
|
|
86
|
+
|
|
87
|
+
def source_workflow(self):
|
|
88
|
+
from .Workflow import Workflow
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
source_workflow = (self.properties or {})["sourceWorkflow"]
|
|
92
|
+
except KeyError:
|
|
93
|
+
self.get()
|
|
94
|
+
if not "sourceWorkflow" in self.properties:
|
|
95
|
+
raise exceptions.ValueError(
|
|
96
|
+
"This datasource doesn't have a source workflow. Use the source_dataset() method instead."
|
|
97
|
+
)
|
|
98
|
+
source_workflow = self.properties["sourceWorkflow"]
|
|
99
|
+
return Workflow(
|
|
100
|
+
source_workflow["qualifiedReference"], properties=source_workflow
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def update(
|
|
104
|
+
self,
|
|
105
|
+
*,
|
|
106
|
+
source_dataset=None,
|
|
107
|
+
source_workflow=None,
|
|
108
|
+
sample=None,
|
|
109
|
+
version=None,
|
|
110
|
+
mapped_tables=None,
|
|
111
|
+
):
|
|
112
|
+
# avoid circular import
|
|
113
|
+
from .Workflow import Workflow
|
|
114
|
+
from .Dataset import Dataset
|
|
115
|
+
from .Version import Version
|
|
116
|
+
|
|
117
|
+
if isinstance(source_workflow, Workflow):
|
|
118
|
+
source_workflow = source_workflow.qualified_reference
|
|
119
|
+
if isinstance(source_dataset, Dataset):
|
|
120
|
+
source_dataset = source_dataset.qualified_reference
|
|
121
|
+
if isinstance(mapped_tables, list):
|
|
122
|
+
mapped_tables_dict = {}
|
|
123
|
+
for source, dest in mapped_tables:
|
|
124
|
+
if isinstance(source, Table):
|
|
125
|
+
source = source.scoped_reference
|
|
126
|
+
if isinstance(dest, Table):
|
|
127
|
+
dest = dest.scoped_reference
|
|
128
|
+
mapped_tables_dict[source] = dest
|
|
129
|
+
mapped_tables = mapped_tables_dict
|
|
130
|
+
|
|
131
|
+
if sample is not None or version is not None:
|
|
132
|
+
if not source_dataset and not source_workflow:
|
|
133
|
+
self.get()
|
|
134
|
+
source_dataset = (self.properties.get("sourceDataset") or {}).get(
|
|
135
|
+
"qualifiedReference"
|
|
136
|
+
)
|
|
137
|
+
if not source_dataset or source_workflow:
|
|
138
|
+
if sample:
|
|
139
|
+
raise exceptions.ValueError(
|
|
140
|
+
"Sampling is not applicable to datasources that reference a workflow"
|
|
141
|
+
)
|
|
142
|
+
if version:
|
|
143
|
+
raise exceptions.ValueError(
|
|
144
|
+
"Versions are not applicable to datasources that reference a workflow"
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
# Keep the existing sample setting, unless it's being changed
|
|
148
|
+
is_sample = (
|
|
149
|
+
sample
|
|
150
|
+
if sample is not None
|
|
151
|
+
else re.search(r":sample\b", source_dataset) is not None
|
|
152
|
+
)
|
|
153
|
+
source_dataset = re.sub(r":sample\b", "", source_dataset)
|
|
154
|
+
|
|
155
|
+
if version is not None:
|
|
156
|
+
if isinstance(version, Version):
|
|
157
|
+
version = version.tag
|
|
158
|
+
source_dataset = Dataset(
|
|
159
|
+
source_dataset, version=version
|
|
160
|
+
).qualified_reference
|
|
161
|
+
|
|
162
|
+
if is_sample:
|
|
163
|
+
source_dataset += ":sample"
|
|
164
|
+
|
|
165
|
+
payload = {
|
|
166
|
+
"sourceDataset": source_dataset,
|
|
167
|
+
"sourceWorkflow": source_workflow,
|
|
168
|
+
"mappedTables": mapped_tables,
|
|
169
|
+
}
|
|
170
|
+
self.properties = make_request(
|
|
171
|
+
method="PATCH",
|
|
172
|
+
path=self.uri,
|
|
173
|
+
# Only change what was specified
|
|
174
|
+
payload={key: value for key, value in payload.items() if value is not None},
|
|
175
|
+
)
|
|
176
|
+
self.uri = self.properties["uri"]
|
|
177
|
+
|
|
178
|
+
return self
|