polydata-client 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {polydata_client-0.1.2 → polydata_client-0.1.3}/CHANGELOG.md +16 -0
- {polydata_client-0.1.2 → polydata_client-0.1.3}/PKG-INFO +1 -1
- {polydata_client-0.1.2 → polydata_client-0.1.3}/polydata/__init__.py +2 -0
- {polydata_client-0.1.2 → polydata_client-0.1.3}/polydata/client.py +64 -6
- {polydata_client-0.1.2 → polydata_client-0.1.3}/polydata/common.py +110 -42
- {polydata_client-0.1.2 → polydata_client-0.1.3}/polydata_client.egg-info/PKG-INFO +1 -1
- {polydata_client-0.1.2 → polydata_client-0.1.3}/pyproject.toml +1 -1
- {polydata_client-0.1.2 → polydata_client-0.1.3}/LICENSE +0 -0
- {polydata_client-0.1.2 → polydata_client-0.1.3}/MANIFEST.in +0 -0
- {polydata_client-0.1.2 → polydata_client-0.1.3}/NOTICE +0 -0
- {polydata_client-0.1.2 → polydata_client-0.1.3}/README.md +0 -0
- {polydata_client-0.1.2 → polydata_client-0.1.3}/polydata_client.egg-info/SOURCES.txt +0 -0
- {polydata_client-0.1.2 → polydata_client-0.1.3}/polydata_client.egg-info/dependency_links.txt +0 -0
- {polydata_client-0.1.2 → polydata_client-0.1.3}/polydata_client.egg-info/requires.txt +0 -0
- {polydata_client-0.1.2 → polydata_client-0.1.3}/polydata_client.egg-info/top_level.txt +0 -0
- {polydata_client-0.1.2 → polydata_client-0.1.3}/setup.cfg +0 -0
|
@@ -5,6 +5,22 @@ All notable changes to the Polydata Python client will be documented in this fil
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.3] - 2026-10-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `update_data_view()` adds `update_method`: `"create_or_replace"` (default) keeps views that read from the updated view, while `"drop_and_create"` allows column changes.
|
|
13
|
+
- `create_component()`, `list_components()` and `list_widgets()`.
|
|
14
|
+
- `create_or_update_component()` and `create_or_update_widget()`, so the same script can create components and widgets on a brand-new hub or update them on an existing one.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `create_or_update_view()` replaces `allow_delete_then_create` with `update_method` (`"create_or_replace"` default, or `"drop_and_create"` when a query changes the view's columns). It no longer guesses from a failed update. With `"drop_and_create"`, `dependent_views` are recreated even if the update fails, and the view keeps its history.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
|
|
22
|
+
- `upload_data_source()` and `upload_large_csv_in_batches()` no longer accept `force_cascade`. A table replace no longer deletes the views that depend on it; if one is in the way, the replace fails with a clear error. To rebuild dependent views, use `create_or_update_view()` with `dependent_views`.
|
|
23
|
+
|
|
8
24
|
## [0.1.2] - 2026-07-23
|
|
9
25
|
|
|
10
26
|
### Added
|
|
@@ -79,7 +79,6 @@ class PolydataClient:
|
|
|
79
79
|
type: str,
|
|
80
80
|
ignore_superset_validation: bool = False,
|
|
81
81
|
explicitly_delete_view: bool = False,
|
|
82
|
-
force_cascade: bool = False,
|
|
83
82
|
) -> Dict:
|
|
84
83
|
"""
|
|
85
84
|
Upload, replace, or append data to a data source.
|
|
@@ -94,8 +93,6 @@ class PolydataClient:
|
|
|
94
93
|
(only applies to replace operations)
|
|
95
94
|
explicitly_delete_view: If True, delete view before replacing
|
|
96
95
|
(only applies to replace operations)
|
|
97
|
-
force_cascade: If True, use CASCADE when dropping columns
|
|
98
|
-
(only applies to replace operations)
|
|
99
96
|
|
|
100
97
|
Returns:
|
|
101
98
|
Dict: Response from the API
|
|
@@ -130,7 +127,6 @@ class PolydataClient:
|
|
|
130
127
|
"explicitly_delete_view": str(
|
|
131
128
|
explicitly_delete_view
|
|
132
129
|
).lower(),
|
|
133
|
-
"force_cascade": str(force_cascade).lower(),
|
|
134
130
|
}
|
|
135
131
|
)
|
|
136
132
|
|
|
@@ -241,7 +237,11 @@ class PolydataClient:
|
|
|
241
237
|
return True
|
|
242
238
|
|
|
243
239
|
def update_data_view(
|
|
244
|
-
self,
|
|
240
|
+
self,
|
|
241
|
+
hub_schema: str,
|
|
242
|
+
view_name: str,
|
|
243
|
+
view_query: str,
|
|
244
|
+
update_method: str = "create_or_replace",
|
|
245
245
|
) -> bool:
|
|
246
246
|
"""
|
|
247
247
|
Update an existing data view using the API endpoint.
|
|
@@ -250,11 +250,14 @@ class PolydataClient:
|
|
|
250
250
|
hub_schema: The hub schema name
|
|
251
251
|
view_name: Name of the view to update
|
|
252
252
|
view_query: New SQL query for the view
|
|
253
|
+
update_method: "create_or_replace" (default) keeps views that read
|
|
254
|
+
from this one, but can't change its columns. "drop_and_create"
|
|
255
|
+
can change columns, but fails if other views read from this one.
|
|
253
256
|
|
|
254
257
|
Returns:
|
|
255
258
|
bool: True if successful, False otherwise
|
|
256
259
|
"""
|
|
257
|
-
payload = {"view_query": view_query}
|
|
260
|
+
payload = {"view_query": view_query, "update_method": update_method}
|
|
258
261
|
response = self.session.patch(
|
|
259
262
|
f"{self.base_url}/data/views/{hub_schema}/{view_name}",
|
|
260
263
|
json=payload,
|
|
@@ -394,6 +397,48 @@ class PolydataClient:
|
|
|
394
397
|
)
|
|
395
398
|
return True
|
|
396
399
|
|
|
400
|
+
def create_component(
|
|
401
|
+
self, hub_schema: str, component_name: str, component_config: dict
|
|
402
|
+
) -> bool:
|
|
403
|
+
"""
|
|
404
|
+
Create a new component using the API endpoint.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
hub_schema: The hub schema name
|
|
408
|
+
component_name: Name for the component
|
|
409
|
+
component_config: Component configuration dict
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
bool: True if successful, False otherwise
|
|
413
|
+
"""
|
|
414
|
+
# Unlike update_component, the create endpoint takes the config as a
|
|
415
|
+
# dict rather than a JSON string.
|
|
416
|
+
payload = {"component_config": component_config}
|
|
417
|
+
|
|
418
|
+
response = self.session.post(
|
|
419
|
+
f"{self.base_url}/apps/components/{hub_schema}/{component_name}",
|
|
420
|
+
json=payload,
|
|
421
|
+
)
|
|
422
|
+
self._handle_response(
|
|
423
|
+
response,
|
|
424
|
+
success_message=f"✓ Created component: {component_name}",
|
|
425
|
+
error_context=f"Create component {component_name}",
|
|
426
|
+
)
|
|
427
|
+
return True
|
|
428
|
+
|
|
429
|
+
def list_components(self, hub_schema: str) -> pd.DataFrame:
|
|
430
|
+
"""List a hub's components as a DataFrame (one row per component)."""
|
|
431
|
+
response = self.session.get(
|
|
432
|
+
f"{self.base_url}/apps/components/{hub_schema}"
|
|
433
|
+
)
|
|
434
|
+
response_data = self._handle_response(
|
|
435
|
+
response, error_context=f"List components for hub {hub_schema}"
|
|
436
|
+
)
|
|
437
|
+
components_list = response_data.get("data", [])
|
|
438
|
+
df = pd.DataFrame(components_list)
|
|
439
|
+
print(f"✓ Retrieved {len(df)} components for hub: {hub_schema}")
|
|
440
|
+
return df
|
|
441
|
+
|
|
397
442
|
def create_widget(
|
|
398
443
|
self, hub_schema: str, widget_name: str, widget_config: dict
|
|
399
444
|
) -> bool:
|
|
@@ -452,6 +497,19 @@ class PolydataClient:
|
|
|
452
497
|
)
|
|
453
498
|
return True
|
|
454
499
|
|
|
500
|
+
def list_widgets(self, hub_schema: str) -> pd.DataFrame:
|
|
501
|
+
"""List a hub's widgets as a DataFrame (one row per widget)."""
|
|
502
|
+
response = self.session.get(
|
|
503
|
+
f"{self.base_url}/apps/widgets/{hub_schema}"
|
|
504
|
+
)
|
|
505
|
+
response_data = self._handle_response(
|
|
506
|
+
response, error_context=f"List widgets for hub {hub_schema}"
|
|
507
|
+
)
|
|
508
|
+
widgets_list = response_data.get("data", [])
|
|
509
|
+
df = pd.DataFrame(widgets_list)
|
|
510
|
+
print(f"✓ Retrieved {len(df)} widgets for hub: {hub_schema}")
|
|
511
|
+
return df
|
|
512
|
+
|
|
455
513
|
def create_ticker(
|
|
456
514
|
self,
|
|
457
515
|
hub_schema: str,
|
|
@@ -30,7 +30,7 @@ def create_or_update_view(
|
|
|
30
30
|
view_name: str,
|
|
31
31
|
view_query: str,
|
|
32
32
|
hub_views_df: pd.DataFrame,
|
|
33
|
-
|
|
33
|
+
update_method: str = "create_or_replace",
|
|
34
34
|
dependent_views: dict | None = None,
|
|
35
35
|
):
|
|
36
36
|
"""
|
|
@@ -45,20 +45,18 @@ def create_or_update_view(
|
|
|
45
45
|
view_name: View name (without schema prefix)
|
|
46
46
|
view_query: SQL query for the view
|
|
47
47
|
hub_views_df: DataFrame from client.list_data_views(hub_schema=hub_schema)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
dependent_views: Mapping of {view_name: view_query} for views that
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
unchanged — but note it must still be valid against the NEW main
|
|
61
|
-
view definition.
|
|
48
|
+
update_method: How an existing view is updated.
|
|
49
|
+
- "create_or_replace" (default): keeps views that read from this
|
|
50
|
+
one, but can't rename, reorder or remove columns.
|
|
51
|
+
- "drop_and_create": use when the query changes the view's
|
|
52
|
+
columns. Views that read from this one block the drop, so list
|
|
53
|
+
them in dependent_views.
|
|
54
|
+
dependent_views: Mapping of {view_name: view_query} for views that read
|
|
55
|
+
from this view. Only used with "drop_and_create": they are deleted
|
|
56
|
+
before the update and recreated after it (in reverse order), even
|
|
57
|
+
if the update fails. Every view needs a query (a ValueError is
|
|
58
|
+
raised before anything is deleted otherwise), and each query must
|
|
59
|
+
be valid against the NEW definition of this view.
|
|
62
60
|
|
|
63
61
|
Example:
|
|
64
62
|
```python
|
|
@@ -87,43 +85,57 @@ def create_or_update_view(
|
|
|
87
85
|
)
|
|
88
86
|
return
|
|
89
87
|
|
|
90
|
-
|
|
88
|
+
valid_update_methods = ["create_or_replace", "drop_and_create"]
|
|
89
|
+
if update_method not in valid_update_methods:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"Invalid update_method '{update_method}'. "
|
|
92
|
+
f"Must be one of: {valid_update_methods}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if update_method == "create_or_replace":
|
|
91
96
|
client.update_data_view(
|
|
92
97
|
hub_schema=hub_schema,
|
|
93
98
|
view_name=view_name,
|
|
94
99
|
view_query=view_query,
|
|
95
100
|
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# In-place update failed (e.g. the query renames/reorders columns,
|
|
100
|
-
# which CREATE OR REPLACE VIEW forbids). Rebuild from scratch:
|
|
101
|
-
# dependents are knocked out first so the plain (non-CASCADE) DROP of
|
|
102
|
-
# the main view succeeds, then restored once it is rebuilt.
|
|
101
|
+
else:
|
|
102
|
+
# Views that read from this one block its (non-CASCADE) drop, so they
|
|
103
|
+
# are deleted first and recreated afterwards, even if the update fails.
|
|
103
104
|
dependent_views = dependent_views or {}
|
|
105
|
+
missing_queries = [
|
|
106
|
+
dependent_name
|
|
107
|
+
for dependent_name, dependent_query in dependent_views.items()
|
|
108
|
+
if not dependent_query or not dependent_query.strip()
|
|
109
|
+
]
|
|
110
|
+
if missing_queries:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"dependent_views {missing_queries} have no view_query, so "
|
|
113
|
+
"they couldn't be recreated after the update."
|
|
114
|
+
)
|
|
115
|
+
|
|
104
116
|
for dependent_name in dependent_views:
|
|
105
117
|
if hub_view_exists(f"{hub_schema}.{dependent_name}", hub_views_df):
|
|
106
118
|
client.delete_data_view(
|
|
107
119
|
hub_schema=hub_schema,
|
|
108
120
|
view_name=dependent_name,
|
|
109
121
|
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
client.create_data_view(
|
|
115
|
-
hub_schema=hub_schema,
|
|
116
|
-
view_name=view_name,
|
|
117
|
-
view_query=view_query,
|
|
118
|
-
)
|
|
119
|
-
for dependent_name, dependent_query in reversed(
|
|
120
|
-
list(dependent_views.items())
|
|
121
|
-
):
|
|
122
|
-
client.create_data_view(
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
client.update_data_view(
|
|
123
125
|
hub_schema=hub_schema,
|
|
124
|
-
view_name=
|
|
125
|
-
view_query=
|
|
126
|
+
view_name=view_name,
|
|
127
|
+
view_query=view_query,
|
|
128
|
+
update_method="drop_and_create",
|
|
126
129
|
)
|
|
130
|
+
finally:
|
|
131
|
+
for dependent_name, dependent_query in reversed(
|
|
132
|
+
list(dependent_views.items())
|
|
133
|
+
):
|
|
134
|
+
client.create_data_view(
|
|
135
|
+
hub_schema=hub_schema,
|
|
136
|
+
view_name=dependent_name,
|
|
137
|
+
view_query=dependent_query,
|
|
138
|
+
)
|
|
127
139
|
|
|
128
140
|
|
|
129
141
|
def ticker_exists(ticker_name: str, hub_tickers_df: pd.DataFrame) -> bool:
|
|
@@ -294,6 +306,66 @@ def create_or_update_endpoint(
|
|
|
294
306
|
)
|
|
295
307
|
|
|
296
308
|
|
|
309
|
+
def component_exists(
|
|
310
|
+
component_name: str, hub_components_df: pd.DataFrame
|
|
311
|
+
) -> bool:
|
|
312
|
+
"""Check if a component exists, given client.list_components() output."""
|
|
313
|
+
if hub_components_df.empty:
|
|
314
|
+
return False
|
|
315
|
+
return component_name in hub_components_df["component_name"].values
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def create_or_update_component(
|
|
319
|
+
client,
|
|
320
|
+
hub_schema: str,
|
|
321
|
+
component_name: str,
|
|
322
|
+
component_config: dict,
|
|
323
|
+
hub_components_df: pd.DataFrame,
|
|
324
|
+
):
|
|
325
|
+
"""Create the component, or update it if it already exists."""
|
|
326
|
+
if component_exists(component_name, hub_components_df):
|
|
327
|
+
client.update_component(
|
|
328
|
+
hub_schema=hub_schema,
|
|
329
|
+
component_name=component_name,
|
|
330
|
+
component_config=component_config,
|
|
331
|
+
)
|
|
332
|
+
else:
|
|
333
|
+
client.create_component(
|
|
334
|
+
hub_schema=hub_schema,
|
|
335
|
+
component_name=component_name,
|
|
336
|
+
component_config=component_config,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def widget_exists(widget_name: str, hub_widgets_df: pd.DataFrame) -> bool:
|
|
341
|
+
"""Check if a widget exists, given client.list_widgets() output."""
|
|
342
|
+
if hub_widgets_df.empty:
|
|
343
|
+
return False
|
|
344
|
+
return widget_name in hub_widgets_df["widget_name"].values
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def create_or_update_widget(
|
|
348
|
+
client,
|
|
349
|
+
hub_schema: str,
|
|
350
|
+
widget_name: str,
|
|
351
|
+
widget_config: dict,
|
|
352
|
+
hub_widgets_df: pd.DataFrame,
|
|
353
|
+
):
|
|
354
|
+
"""Create the widget, or update it if it already exists."""
|
|
355
|
+
if widget_exists(widget_name, hub_widgets_df):
|
|
356
|
+
client.update_widget(
|
|
357
|
+
hub_schema=hub_schema,
|
|
358
|
+
widget_name=widget_name,
|
|
359
|
+
widget_config=widget_config,
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
client.create_widget(
|
|
363
|
+
hub_schema=hub_schema,
|
|
364
|
+
widget_name=widget_name,
|
|
365
|
+
widget_config=widget_config,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
297
369
|
def upload_large_csv_in_batches(
|
|
298
370
|
client,
|
|
299
371
|
file_path: str,
|
|
@@ -304,7 +376,6 @@ def upload_large_csv_in_batches(
|
|
|
304
376
|
delay_between_chunks: int = 5,
|
|
305
377
|
ignore_superset_validation: bool = False,
|
|
306
378
|
explicitly_delete_view: bool = False,
|
|
307
|
-
force_cascade: bool = False,
|
|
308
379
|
) -> Dict:
|
|
309
380
|
"""
|
|
310
381
|
Upload a large CSV file in batches to avoid memory issues.
|
|
@@ -330,8 +401,6 @@ def upload_large_csv_in_batches(
|
|
|
330
401
|
(only applies to first/replace operation)
|
|
331
402
|
explicitly_delete_view: If True, delete view before replacing
|
|
332
403
|
(only applies to first/replace operation)
|
|
333
|
-
force_cascade: If True, use CASCADE when dropping columns
|
|
334
|
-
(only applies to first/replace operation)
|
|
335
404
|
|
|
336
405
|
Returns:
|
|
337
406
|
Dict: Summary of upload results
|
|
@@ -406,7 +475,6 @@ def upload_large_csv_in_batches(
|
|
|
406
475
|
type="replace",
|
|
407
476
|
ignore_superset_validation=ignore_superset_validation,
|
|
408
477
|
explicitly_delete_view=explicitly_delete_view,
|
|
409
|
-
force_cascade=force_cascade,
|
|
410
478
|
)
|
|
411
479
|
else:
|
|
412
480
|
client.upload_data_source(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{polydata_client-0.1.2 → polydata_client-0.1.3}/polydata_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|