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 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"
@@ -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