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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polydata-client
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Official Python client for Polydata API
5
5
  Author-email: Polydata <support@polydata.co>
6
6
  Maintainer-email: Polydata <support@polydata.co>
@@ -1,8 +1,10 @@
1
1
  from .client import PolydataClient
2
2
  from .common import (
3
+ create_or_update_component,
3
4
  create_or_update_endpoint,
4
5
  create_or_update_ticker,
5
6
  create_or_update_view,
7
+ create_or_update_widget,
6
8
  )
7
9
 
8
10
  __version__ = "0.1.0"
@@ -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, hub_schema: str, view_name: str, view_query: str
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
- allow_delete_then_create: bool = False,
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
- allow_delete_then_create: If True and an in-place update fails, fall back
49
- to deleting and recreating the view. The update uses CREATE OR
50
- REPLACE VIEW, which cannot rename or reorder existing columns; enable
51
- this when a query change alters the view's column set. Off by default
52
- because delete-then-create is more destructive. When False, an update
53
- failure propagates unchanged.
54
- dependent_views: Mapping of {view_name: view_query} for views that
55
- depend on this view and would block its DROP. Only consulted on the
56
- delete-then-create fallback: dependents are deleted first (in the
57
- given order) so the main view can be dropped without CASCADE, then
58
- recreated from the provided queries (in reverse order) once the
59
- main view is rebuilt. Pass each dependent's current query if it is
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
- try:
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
- except Exception:
97
- if not allow_delete_then_create:
98
- raise
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
- client.delete_data_view(
111
- hub_schema=hub_schema,
112
- view_name=view_name,
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=dependent_name,
125
- view_query=dependent_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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polydata-client
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Official Python client for Polydata API
5
5
  Author-email: Polydata <support@polydata.co>
6
6
  Maintainer-email: Polydata <support@polydata.co>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "polydata-client"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Official Python client for Polydata API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
File without changes
File without changes