polydata-client 0.1.1__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.
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ All notable changes to the Polydata Python client will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
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
+
24
+ ## [0.1.2] - 2026-07-23
25
+
26
+ ### Added
27
+
28
+ - `create_or_update_view()` adds `allow_delete_then_create` option, which helps creators who change their column names.
29
+ - `create_or_update_view()` also adds `dependent_views` which are dropped and rebuilt around the recreate.
30
+
31
+ ## [0.1.1] - 2026-01-27
32
+
33
+ ### Added
34
+
35
+ - Added utilities to manage and update the cache
36
+ - `client.get_cache_stats()` Gets the Redis cache statistics.
37
+ - `client.clear_hub_cache()` Clears all cached data app pages for a Hub.
38
+
39
+ ## [0.1.0] - 2026-01-16
40
+
41
+ ### Added
42
+
43
+ - Initial release of Polydata Python client
44
+ - Support for data source uploads (create, replace, append)
45
+ - Data view management (create, update, get, list)
46
+ - Data app creation and page updates
47
+ - Component and widget management
48
+ - Ticker creation and updates
49
+ - Custom endpoint creation and management
50
+ - Helper utilities for create-or-update patterns
51
+ - Batch upload support for large CSV files
52
+ - Comprehensive error handling
53
+ - API connection testing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polydata-client
3
- Version: 0.1.1
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>
@@ -0,0 +1,10 @@
1
+ from .client import PolydataClient
2
+ from .common import (
3
+ create_or_update_component,
4
+ create_or_update_endpoint,
5
+ create_or_update_ticker,
6
+ create_or_update_view,
7
+ create_or_update_widget,
8
+ )
9
+
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,
@@ -266,6 +269,30 @@ class PolydataClient:
266
269
  )
267
270
  return True
268
271
 
272
+ def delete_data_view(self, hub_schema: str, view_name: str) -> bool:
273
+ """
274
+ Delete an existing data view using the API endpoint.
275
+
276
+ Drops both the physical Postgres view and its hub_views metadata
277
+ record.
278
+
279
+ Args:
280
+ hub_schema: The hub schema name
281
+ view_name: Name of the view to delete
282
+
283
+ Returns:
284
+ bool: True if successful, False otherwise
285
+ """
286
+ response = self.session.delete(
287
+ f"{self.base_url}/data/views/{hub_schema}/{view_name}",
288
+ )
289
+ self._handle_response(
290
+ response,
291
+ success_message=f"✓ Deleted data view: {view_name}",
292
+ error_context=f"Delete view {view_name}",
293
+ )
294
+ return True
295
+
269
296
  def get_data_view(
270
297
  self, hub_schema: str, view_name: str, include_data: bool = True
271
298
  ) -> pd.DataFrame:
@@ -370,6 +397,48 @@ class PolydataClient:
370
397
  )
371
398
  return True
372
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
+
373
442
  def create_widget(
374
443
  self, hub_schema: str, widget_name: str, widget_config: dict
375
444
  ) -> bool:
@@ -428,6 +497,19 @@ class PolydataClient:
428
497
  )
429
498
  return True
430
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
+
431
513
  def create_ticker(
432
514
  self,
433
515
  hub_schema: str,
@@ -719,4 +801,4 @@ class PolydataClient:
719
801
  f" - Cleared {deleted} cached pages out of {cached} that were cached ({total} total pages)"
720
802
  )
721
803
 
722
- return True
804
+ return True
@@ -30,6 +30,8 @@ def create_or_update_view(
30
30
  view_name: str,
31
31
  view_query: str,
32
32
  hub_views_df: pd.DataFrame,
33
+ update_method: str = "create_or_replace",
34
+ dependent_views: dict | None = None,
33
35
  ):
34
36
  """
35
37
  Create or update a data view based on whether it exists.
@@ -43,6 +45,18 @@ def create_or_update_view(
43
45
  view_name: View name (without schema prefix)
44
46
  view_query: SQL query for the view
45
47
  hub_views_df: DataFrame from client.list_data_views(hub_schema=hub_schema)
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.
46
60
 
47
61
  Example:
48
62
  ```python
@@ -63,18 +77,65 @@ def create_or_update_view(
63
77
  """
64
78
  full_view_name = f"{hub_schema}.{view_name}"
65
79
 
66
- if hub_view_exists(full_view_name, hub_views_df):
67
- client.update_data_view(
80
+ if not hub_view_exists(full_view_name, hub_views_df):
81
+ client.create_data_view(
68
82
  hub_schema=hub_schema,
69
83
  view_name=view_name,
70
84
  view_query=view_query,
71
85
  )
72
- else:
73
- client.create_data_view(
86
+ return
87
+
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":
96
+ client.update_data_view(
74
97
  hub_schema=hub_schema,
75
98
  view_name=view_name,
76
99
  view_query=view_query,
77
100
  )
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.
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
+
116
+ for dependent_name in dependent_views:
117
+ if hub_view_exists(f"{hub_schema}.{dependent_name}", hub_views_df):
118
+ client.delete_data_view(
119
+ hub_schema=hub_schema,
120
+ view_name=dependent_name,
121
+ )
122
+
123
+ try:
124
+ client.update_data_view(
125
+ hub_schema=hub_schema,
126
+ view_name=view_name,
127
+ view_query=view_query,
128
+ update_method="drop_and_create",
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
+ )
78
139
 
79
140
 
80
141
  def ticker_exists(ticker_name: str, hub_tickers_df: pd.DataFrame) -> bool:
@@ -161,6 +222,150 @@ def create_or_update_ticker(
161
222
  )
162
223
 
163
224
 
225
+ def endpoint_exists(endpoint_name: str, hub_endpoints_df: pd.DataFrame) -> bool:
226
+ """
227
+ Check if an endpoint exists in the hub.
228
+
229
+ Args:
230
+ endpoint_name: Endpoint name to check
231
+ hub_endpoints_df: DataFrame from client.list_endpoints()
232
+
233
+ Returns:
234
+ True if the endpoint exists, False otherwise
235
+ """
236
+ if hub_endpoints_df.empty:
237
+ return False
238
+ return endpoint_name in hub_endpoints_df["hub_endpoint_name"].values
239
+
240
+
241
+ def create_or_update_endpoint(
242
+ client,
243
+ hub_schema: str,
244
+ endpoint_name: str,
245
+ view_name: str,
246
+ params_config: dict,
247
+ hub_views_df: pd.DataFrame,
248
+ hub_endpoints_df: pd.DataFrame,
249
+ subscription_plan_type_name: str = "enterprise",
250
+ ):
251
+ """
252
+ Create or update an endpoint based on whether it exists.
253
+
254
+ Mirrors create_or_update_view: checks hub_endpoints_df and calls either
255
+ client.update_endpoint() or client.create_endpoint() (resolving the backing
256
+ view's id from hub_views_df) accordingly.
257
+
258
+ Args:
259
+ client: PolydataClient instance
260
+ hub_schema: Schema name
261
+ endpoint_name: Human-readable endpoint name
262
+ view_name: Backing view name (without schema prefix)
263
+ params_config: Endpoint params config (allowed_params/allowed_columns)
264
+ hub_views_df: DataFrame from client.list_data_views()
265
+ hub_endpoints_df: DataFrame from client.list_endpoints()
266
+ subscription_plan_type_name: Plan required when creating the endpoint
267
+ (default: "enterprise"); ignored on update
268
+
269
+ Example:
270
+ ```python
271
+ from polydata import PolydataClient
272
+ from polydata.common import create_or_update_endpoint
273
+
274
+ client = PolydataClient(api_key="...", base_url="...")
275
+ hub_views_df = client.list_data_views(hub_schema="my_schema")
276
+ hub_endpoints_df = client.list_endpoints(hub_schema="my_schema")
277
+
278
+ create_or_update_endpoint(
279
+ client=client,
280
+ hub_schema="my_schema",
281
+ endpoint_name="Available Leagues",
282
+ view_name="available_leagues",
283
+ params_config={...},
284
+ hub_views_df=hub_views_df,
285
+ hub_endpoints_df=hub_endpoints_df,
286
+ )
287
+ ```
288
+ """
289
+ if endpoint_exists(endpoint_name, hub_endpoints_df):
290
+ client.update_endpoint(
291
+ hub_schema=hub_schema,
292
+ hub_endpoint_name=endpoint_name,
293
+ params_config=params_config,
294
+ )
295
+ else:
296
+ full_view_name = f"{hub_schema}.{view_name}"
297
+ matching = hub_views_df[hub_views_df["view_name"] == full_view_name]
298
+ if matching.empty:
299
+ raise ValueError(f"Could not find view {full_view_name}")
300
+ client.create_endpoint(
301
+ hub_schema=hub_schema,
302
+ hub_view_id=int(matching.iloc[0]["hub_view_id"]),
303
+ hub_endpoint_name=endpoint_name,
304
+ subscription_plan_type_name=subscription_plan_type_name,
305
+ params_config=params_config,
306
+ )
307
+
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
+
164
369
  def upload_large_csv_in_batches(
165
370
  client,
166
371
  file_path: str,
@@ -171,7 +376,6 @@ def upload_large_csv_in_batches(
171
376
  delay_between_chunks: int = 5,
172
377
  ignore_superset_validation: bool = False,
173
378
  explicitly_delete_view: bool = False,
174
- force_cascade: bool = False,
175
379
  ) -> Dict:
176
380
  """
177
381
  Upload a large CSV file in batches to avoid memory issues.
@@ -197,8 +401,6 @@ def upload_large_csv_in_batches(
197
401
  (only applies to first/replace operation)
198
402
  explicitly_delete_view: If True, delete view before replacing
199
403
  (only applies to first/replace operation)
200
- force_cascade: If True, use CASCADE when dropping columns
201
- (only applies to first/replace operation)
202
404
 
203
405
  Returns:
204
406
  Dict: Summary of upload results
@@ -273,7 +475,6 @@ def upload_large_csv_in_batches(
273
475
  type="replace",
274
476
  ignore_superset_validation=ignore_superset_validation,
275
477
  explicitly_delete_view=explicitly_delete_view,
276
- force_cascade=force_cascade,
277
478
  )
278
479
  else:
279
480
  client.upload_data_source(
@@ -299,7 +500,6 @@ def upload_large_csv_in_batches(
299
500
  time.sleep(delay_between_chunks)
300
501
 
301
502
  finally:
302
- # Clean up temporary directory
303
503
  try:
304
504
  os.rmdir(temp_dir)
305
505
  print("\n 🧹 Cleaned up temporary directory")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polydata-client
3
- Version: 0.1.1
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.1"
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"
@@ -1,31 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to the Polydata Python client will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [0.1.1] - 2026-01-27
9
-
10
- ### Added
11
-
12
- - Added utilities to manage and update the cache
13
- - `client.get_cache_stats()` Gets the Redis cache statistics.
14
- - `client.clear_hub_cache()` Clears all cached data app pages for a Hub.
15
-
16
-
17
- ## [0.1.0] - 2026-01-16
18
-
19
- ### Added
20
-
21
- - Initial release of Polydata Python client
22
- - Support for data source uploads (create, replace, append)
23
- - Data view management (create, update, get, list)
24
- - Data app creation and page updates
25
- - Component and widget management
26
- - Ticker creation and updates
27
- - Custom endpoint creation and management
28
- - Helper utilities for create-or-update patterns
29
- - Batch upload support for large CSV files
30
- - Comprehensive error handling
31
- - API connection testing
@@ -1,4 +0,0 @@
1
- from .client import PolydataClient
2
- from .common import create_or_update_ticker, create_or_update_view
3
-
4
- __version__ = "0.1.0"
File without changes
File without changes