polydata-client 0.1.0__tar.gz → 0.1.2__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,21 @@ 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.2] - 2026-07-23
9
+
10
+ ### Added
11
+
12
+ - `create_or_update_view()` adds `allow_delete_then_create` option, which helps creators who change their column names.
13
+ - `create_or_update_view()` also adds `dependent_views` which are dropped and rebuilt around the recreate.
14
+
15
+ ## [0.1.1] - 2026-01-27
16
+
17
+ ### Added
18
+
19
+ - Added utilities to manage and update the cache
20
+ - `client.get_cache_stats()` Gets the Redis cache statistics.
21
+ - `client.clear_hub_cache()` Clears all cached data app pages for a Hub.
22
+
8
23
  ## [0.1.0] - 2026-01-16
9
24
 
10
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polydata-client
3
- Version: 0.1.0
3
+ Version: 0.1.2
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,8 @@
1
+ from .client import PolydataClient
2
+ from .common import (
3
+ create_or_update_endpoint,
4
+ create_or_update_ticker,
5
+ create_or_update_view,
6
+ )
7
+
8
+ __version__ = "0.1.0"
@@ -266,6 +266,30 @@ class PolydataClient:
266
266
  )
267
267
  return True
268
268
 
269
+ def delete_data_view(self, hub_schema: str, view_name: str) -> bool:
270
+ """
271
+ Delete an existing data view using the API endpoint.
272
+
273
+ Drops both the physical Postgres view and its hub_views metadata
274
+ record.
275
+
276
+ Args:
277
+ hub_schema: The hub schema name
278
+ view_name: Name of the view to delete
279
+
280
+ Returns:
281
+ bool: True if successful, False otherwise
282
+ """
283
+ response = self.session.delete(
284
+ f"{self.base_url}/data/views/{hub_schema}/{view_name}",
285
+ )
286
+ self._handle_response(
287
+ response,
288
+ success_message=f"✓ Deleted data view: {view_name}",
289
+ error_context=f"Delete view {view_name}",
290
+ )
291
+ return True
292
+
269
293
  def get_data_view(
270
294
  self, hub_schema: str, view_name: str, include_data: bool = True
271
295
  ) -> pd.DataFrame:
@@ -664,3 +688,59 @@ class PolydataClient:
664
688
  df = pd.DataFrame(endpoints_list)
665
689
  print(f"✓ Retrieved {len(df)} endpoints for hub: {hub_schema}")
666
690
  return df
691
+
692
+ def get_cache_stats(self) -> dict:
693
+ """
694
+ Get Redis cache statistics.
695
+
696
+ Returns:
697
+ Dict: Cache statistics including total keys and memory usage
698
+
699
+ Raises:
700
+ Exception: If retrieval fails
701
+ """
702
+ response = self.session.get(f"{self.base_url}/cache/stats")
703
+ response_data = self._handle_response(
704
+ response, error_context="Get cache stats"
705
+ )
706
+ stats = response_data.get("data", {})
707
+ total_keys = stats.get("total_keys", 0)
708
+ print(f"✓ Cache stats retrieved: {total_keys} total keys")
709
+ return response_data
710
+
711
+ def clear_hub_cache(
712
+ self, hub_schema_name: str, entity_type: str = "data_app_page"
713
+ ) -> bool:
714
+ """
715
+ Clear all cached pages for a hub.
716
+
717
+ This is useful after uploading new data to ensure users see fresh data
718
+ without having to rebuild the entire app.
719
+
720
+ Args:
721
+ hub_schema_name: The hub schema name (e.g., "football_player_ratings")
722
+
723
+ Returns:
724
+ bool: True if successful, False otherwise
725
+
726
+ Raises:
727
+ Exception: If cache clear fails
728
+ """
729
+ response = self.session.delete(
730
+ f"{self.base_url}/cache/{hub_schema_name}/{entity_type}"
731
+ )
732
+ response_data = self._handle_response(
733
+ response,
734
+ success_message=f"✓ Cleared cache for hub: {hub_schema_name}",
735
+ error_context=f"Clear cache for hub {hub_schema_name}",
736
+ )
737
+
738
+ data = response_data.get("data", {})
739
+ deleted = data.get("deleted_count", 0)
740
+ total = data.get("total_pages_in_hub", 0)
741
+ cached = data.get("cached_pages", 0)
742
+ print(
743
+ f" - Cleared {deleted} cached pages out of {cached} that were cached ({total} total pages)"
744
+ )
745
+
746
+ 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
+ allow_delete_then_create: bool = False,
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,20 @@ 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
+ 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.
46
62
 
47
63
  Example:
48
64
  ```python
@@ -63,18 +79,51 @@ def create_or_update_view(
63
79
  """
64
80
  full_view_name = f"{hub_schema}.{view_name}"
65
81
 
66
- if hub_view_exists(full_view_name, hub_views_df):
82
+ if not hub_view_exists(full_view_name, hub_views_df):
83
+ client.create_data_view(
84
+ hub_schema=hub_schema,
85
+ view_name=view_name,
86
+ view_query=view_query,
87
+ )
88
+ return
89
+
90
+ try:
67
91
  client.update_data_view(
68
92
  hub_schema=hub_schema,
69
93
  view_name=view_name,
70
94
  view_query=view_query,
71
95
  )
72
- else:
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.
103
+ dependent_views = dependent_views or {}
104
+ for dependent_name in dependent_views:
105
+ if hub_view_exists(f"{hub_schema}.{dependent_name}", hub_views_df):
106
+ client.delete_data_view(
107
+ hub_schema=hub_schema,
108
+ view_name=dependent_name,
109
+ )
110
+ client.delete_data_view(
111
+ hub_schema=hub_schema,
112
+ view_name=view_name,
113
+ )
73
114
  client.create_data_view(
74
115
  hub_schema=hub_schema,
75
116
  view_name=view_name,
76
117
  view_query=view_query,
77
118
  )
119
+ for dependent_name, dependent_query in reversed(
120
+ list(dependent_views.items())
121
+ ):
122
+ client.create_data_view(
123
+ hub_schema=hub_schema,
124
+ view_name=dependent_name,
125
+ view_query=dependent_query,
126
+ )
78
127
 
79
128
 
80
129
  def ticker_exists(ticker_name: str, hub_tickers_df: pd.DataFrame) -> bool:
@@ -161,6 +210,90 @@ def create_or_update_ticker(
161
210
  )
162
211
 
163
212
 
213
+ def endpoint_exists(endpoint_name: str, hub_endpoints_df: pd.DataFrame) -> bool:
214
+ """
215
+ Check if an endpoint exists in the hub.
216
+
217
+ Args:
218
+ endpoint_name: Endpoint name to check
219
+ hub_endpoints_df: DataFrame from client.list_endpoints()
220
+
221
+ Returns:
222
+ True if the endpoint exists, False otherwise
223
+ """
224
+ if hub_endpoints_df.empty:
225
+ return False
226
+ return endpoint_name in hub_endpoints_df["hub_endpoint_name"].values
227
+
228
+
229
+ def create_or_update_endpoint(
230
+ client,
231
+ hub_schema: str,
232
+ endpoint_name: str,
233
+ view_name: str,
234
+ params_config: dict,
235
+ hub_views_df: pd.DataFrame,
236
+ hub_endpoints_df: pd.DataFrame,
237
+ subscription_plan_type_name: str = "enterprise",
238
+ ):
239
+ """
240
+ Create or update an endpoint based on whether it exists.
241
+
242
+ Mirrors create_or_update_view: checks hub_endpoints_df and calls either
243
+ client.update_endpoint() or client.create_endpoint() (resolving the backing
244
+ view's id from hub_views_df) accordingly.
245
+
246
+ Args:
247
+ client: PolydataClient instance
248
+ hub_schema: Schema name
249
+ endpoint_name: Human-readable endpoint name
250
+ view_name: Backing view name (without schema prefix)
251
+ params_config: Endpoint params config (allowed_params/allowed_columns)
252
+ hub_views_df: DataFrame from client.list_data_views()
253
+ hub_endpoints_df: DataFrame from client.list_endpoints()
254
+ subscription_plan_type_name: Plan required when creating the endpoint
255
+ (default: "enterprise"); ignored on update
256
+
257
+ Example:
258
+ ```python
259
+ from polydata import PolydataClient
260
+ from polydata.common import create_or_update_endpoint
261
+
262
+ client = PolydataClient(api_key="...", base_url="...")
263
+ hub_views_df = client.list_data_views(hub_schema="my_schema")
264
+ hub_endpoints_df = client.list_endpoints(hub_schema="my_schema")
265
+
266
+ create_or_update_endpoint(
267
+ client=client,
268
+ hub_schema="my_schema",
269
+ endpoint_name="Available Leagues",
270
+ view_name="available_leagues",
271
+ params_config={...},
272
+ hub_views_df=hub_views_df,
273
+ hub_endpoints_df=hub_endpoints_df,
274
+ )
275
+ ```
276
+ """
277
+ if endpoint_exists(endpoint_name, hub_endpoints_df):
278
+ client.update_endpoint(
279
+ hub_schema=hub_schema,
280
+ hub_endpoint_name=endpoint_name,
281
+ params_config=params_config,
282
+ )
283
+ else:
284
+ full_view_name = f"{hub_schema}.{view_name}"
285
+ matching = hub_views_df[hub_views_df["view_name"] == full_view_name]
286
+ if matching.empty:
287
+ raise ValueError(f"Could not find view {full_view_name}")
288
+ client.create_endpoint(
289
+ hub_schema=hub_schema,
290
+ hub_view_id=int(matching.iloc[0]["hub_view_id"]),
291
+ hub_endpoint_name=endpoint_name,
292
+ subscription_plan_type_name=subscription_plan_type_name,
293
+ params_config=params_config,
294
+ )
295
+
296
+
164
297
  def upload_large_csv_in_batches(
165
298
  client,
166
299
  file_path: str,
@@ -299,7 +432,6 @@ def upload_large_csv_in_batches(
299
432
  time.sleep(delay_between_chunks)
300
433
 
301
434
  finally:
302
- # Clean up temporary directory
303
435
  try:
304
436
  os.rmdir(temp_dir)
305
437
  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.0
3
+ Version: 0.1.2
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.0"
3
+ version = "0.1.2"
4
4
  description = "Official Python client for Polydata API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
@@ -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