plato-sdk-v2 2.7.6__py3-none-any.whl → 2.7.7__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.
plato/cli/audit_ui.py ADDED
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Streamlit UI to configure ignore_tables and ignore_columns.
4
+
5
+ Usage: plato sandbox audit-ui
6
+ """
7
+
8
+ import json
9
+ import os
10
+ from typing import Any
11
+
12
+ import streamlit as st
13
+
14
+
15
+ def get_env_db_config() -> dict | None:
16
+ """Get DB config from environment variables set by plato sandbox audit-ui."""
17
+ host = os.environ.get("PLATO_DB_HOST")
18
+ port = os.environ.get("PLATO_DB_PORT")
19
+ user = os.environ.get("PLATO_DB_USER")
20
+ password = os.environ.get("PLATO_DB_PASSWORD", "")
21
+ database = os.environ.get("PLATO_DB_NAME")
22
+ db_type = os.environ.get("PLATO_DB_TYPE")
23
+
24
+ if host and port and user and database:
25
+ return {
26
+ "host": host,
27
+ "port": int(port),
28
+ "user": user,
29
+ "password": password,
30
+ "database": database,
31
+ "db_type": db_type or "postgresql",
32
+ }
33
+ return None
34
+
35
+
36
+ def connect_db(db_type, host, port, user, password, database):
37
+ if db_type == "postgresql" or port == 5432:
38
+ import psycopg2
39
+
40
+ return psycopg2.connect(host=host, port=port, user=user, password=password, database=database), "postgresql"
41
+ else:
42
+ import pymysql
43
+
44
+ return pymysql.connect(host=host, port=port, user=user, password=password, database=database), "mysql"
45
+
46
+
47
+ def load_all_data(conn, db_type):
48
+ cursor = conn.cursor()
49
+
50
+ if db_type == "postgresql":
51
+ cursor.execute(
52
+ "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ORDER BY table_name"
53
+ )
54
+ else:
55
+ cursor.execute("SHOW TABLES")
56
+
57
+ tables = [row[0] for row in cursor.fetchall()]
58
+ all_data = {}
59
+
60
+ for table in tables:
61
+ quote = '"' if db_type == "postgresql" else "`"
62
+
63
+ # Columns
64
+ if db_type == "postgresql":
65
+ cursor.execute(
66
+ f"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '{table}' AND table_schema = 'public' ORDER BY ordinal_position"
67
+ )
68
+ cols = [{"name": r[0], "type": r[1]} for r in cursor.fetchall()]
69
+ else:
70
+ cursor.execute(f"DESCRIBE `{table}`")
71
+ cols = [{"name": r[0], "type": str(r[1])} for r in cursor.fetchall()]
72
+
73
+ # Count
74
+ try:
75
+ cursor.execute(f"SELECT COUNT(*) FROM {quote}{table}{quote}")
76
+ count = cursor.fetchone()[0]
77
+ except Exception:
78
+ count = 0
79
+
80
+ # Example values per column
81
+ col_examples = {}
82
+ for col in cols[:20]: # Limit to first 20 columns for performance
83
+ try:
84
+ cursor.execute(
85
+ f"SELECT DISTINCT {quote}{col['name']}{quote} FROM {quote}{table}{quote} WHERE {quote}{col['name']}{quote} IS NOT NULL LIMIT 3"
86
+ )
87
+ values = [str(row[0])[:40] for row in cursor.fetchall()]
88
+ col_examples[col["name"]] = values
89
+ except Exception:
90
+ col_examples[col["name"]] = []
91
+
92
+ all_data[table] = {"columns": cols, "count": count, "col_examples": col_examples}
93
+
94
+ return tables, all_data
95
+
96
+
97
+ def main():
98
+ st.set_page_config(page_title="Ignore Tables", layout="wide")
99
+ st.title("🔍 Ignore Tables & Columns")
100
+
101
+ # Initialize ALL state first
102
+ if "ignore_tables" not in st.session_state:
103
+ st.session_state.ignore_tables = set()
104
+ if "ignore_cols_global" not in st.session_state:
105
+ st.session_state.ignore_cols_global = set()
106
+ if "ignore_cols_per_table" not in st.session_state:
107
+ st.session_state.ignore_cols_per_table = {}
108
+ if "show_columns" not in st.session_state:
109
+ st.session_state.show_columns = False
110
+
111
+ # Check for env config from plato sandbox audit-ui
112
+ env_config = get_env_db_config()
113
+
114
+ # Sidebar - use env config or query params
115
+ st.sidebar.header("📡 Connection")
116
+
117
+ qp = st.query_params
118
+
119
+ # Use env config as defaults if available
120
+ default_host = env_config["host"] if env_config else qp.get("host", "127.0.0.1")
121
+ default_port = env_config["port"] if env_config else int(qp.get("port", "3306"))
122
+ default_user = env_config["user"] if env_config else qp.get("user", "root")
123
+ default_password = env_config["password"] if env_config else qp.get("password", "")
124
+ default_database = env_config["database"] if env_config else qp.get("database", "")
125
+
126
+ # Show connection info if from env
127
+ if env_config:
128
+ st.sidebar.success(f"🔗 Tunnel active: {default_user}@{default_host}:{default_port}/{default_database}")
129
+
130
+ host = st.sidebar.text_input("Host", default_host)
131
+ port = st.sidebar.number_input("Port", min_value=1, max_value=65535, value=default_port)
132
+ user = st.sidebar.text_input("User", default_user)
133
+ password = st.sidebar.text_input("Password", default_password, type="password")
134
+ database = st.sidebar.text_input("Database", default_database)
135
+
136
+ # Update query params
137
+ st.query_params.update({"host": host, "port": str(port), "user": user, "password": password, "database": database})
138
+
139
+ if not env_config:
140
+ st.sidebar.markdown("---")
141
+ st.sidebar.markdown("**💡 Tip:** Use `plato sandbox tunnel <port>` to forward a remote DB port to localhost.")
142
+
143
+ if st.sidebar.button("🔌 Connect & Load", type="primary"):
144
+ with st.spinner("Loading..."):
145
+ try:
146
+ db_type = env_config["db_type"] if env_config else ("postgresql" if port == 5432 else "mysql")
147
+ conn, db_type = connect_db(db_type, host, port, user, password, database)
148
+ tables, all_data = load_all_data(conn, db_type)
149
+ st.session_state.tables = tables
150
+ st.session_state.all_data = all_data
151
+ st.sidebar.success(f"✅ {len(tables)} tables")
152
+ except Exception as e:
153
+ error_str = str(e)
154
+ st.sidebar.error(f"❌ {error_str}")
155
+
156
+ if "Connection refused" in error_str or "connection to server" in error_str.lower():
157
+ st.sidebar.warning(
158
+ "💡 **Hint:** Run `plato sandbox tunnel <db_port>` to forward the database port."
159
+ )
160
+
161
+ import traceback
162
+
163
+ st.sidebar.code(traceback.format_exc())
164
+ return
165
+
166
+ if "tables" not in st.session_state:
167
+ st.info("👈 Connect to a database to start")
168
+ return
169
+
170
+ tables = st.session_state.tables
171
+ all_data = st.session_state.all_data
172
+
173
+ st.sidebar.write(f"🚫 {len(st.session_state.ignore_tables)} tables")
174
+ st.sidebar.write(f"🌐 {len(st.session_state.ignore_cols_global)} global")
175
+
176
+ # Global filters
177
+ st.write("### 🌐 Global Column Filters (ignored in ALL tables)")
178
+
179
+ col1, col2 = st.columns([3, 1])
180
+ with col1:
181
+ new_global = st.text_input(
182
+ "Add column name:",
183
+ placeholder="e.g., updated_at, created_at, modified_by",
184
+ key="add_global",
185
+ )
186
+ with col2:
187
+ st.write("") # Spacing
188
+ if st.button("➕ Add", key="add_global_btn") and new_global:
189
+ st.session_state.ignore_cols_global.add(new_global)
190
+
191
+ if st.session_state.ignore_cols_global:
192
+ st.write("**Currently ignored globally:**")
193
+ cols_display = st.columns(4)
194
+ for i, col in enumerate(sorted(st.session_state.ignore_cols_global)):
195
+ with cols_display[i % 4]:
196
+ if st.button(f"❌ {col}", key=f"rm_global_{col}", help="Click to remove"):
197
+ st.session_state.ignore_cols_global.discard(col)
198
+ else:
199
+ st.caption("No global column filters set")
200
+
201
+ st.write("---")
202
+
203
+ # Tables section
204
+ col1, col2 = st.columns([4, 1])
205
+ with col1:
206
+ search = st.text_input("🔍 Search", "")
207
+ with col2:
208
+ show_cols = st.toggle("Show Columns", value=st.session_state.show_columns)
209
+ st.session_state.show_columns = show_cols
210
+
211
+ filtered = [t for t in tables if search.lower() in t.lower()] if search else tables
212
+
213
+ st.write(f"### Tables ({len(filtered)})")
214
+
215
+ # Display tables
216
+ for table in filtered:
217
+ is_ignored = table in st.session_state.ignore_tables
218
+ data = all_data[table]
219
+
220
+ # Table header with expand/collapse
221
+ name = f"~~{table}~~" if is_ignored else table
222
+ icon = "🚫" if is_ignored else "📋"
223
+
224
+ # Expand if show_columns is on
225
+ with st.expander(f"{icon} {name} — {data['count']} rows", expanded=st.session_state.show_columns):
226
+ # Ignore table checkbox
227
+ ign = st.checkbox("🚫 Ignore entire table", value=is_ignored, key=f"t_{table}")
228
+ if ign:
229
+ st.session_state.ignore_tables.add(table)
230
+ else:
231
+ st.session_state.ignore_tables.discard(table)
232
+
233
+ # Show columns if not ignored
234
+ if not ign:
235
+ if table not in st.session_state.ignore_cols_per_table:
236
+ st.session_state.ignore_cols_per_table[table] = set()
237
+
238
+ # Column header
239
+ st.markdown("**Columns:**")
240
+ h1, h2, h3, h4 = st.columns([0.5, 2, 1.5, 4], gap="small")
241
+ h1.caption("Ign")
242
+ h2.caption("Name")
243
+ h3.caption("Type")
244
+ h4.caption("Examples")
245
+
246
+ for col_idx, col in enumerate(data["columns"]):
247
+ col_name = col["name"]
248
+ is_global = col_name in st.session_state.ignore_cols_global
249
+ is_local = col_name in st.session_state.ignore_cols_per_table[table]
250
+
251
+ examples = data["col_examples"].get(col_name, [])
252
+ ex_str = ", ".join(examples)
253
+
254
+ c1, c2, c3, c4 = st.columns([0.5, 2, 1.5, 4], gap="small")
255
+ with c1:
256
+ if is_global:
257
+ st.checkbox(
258
+ "",
259
+ True,
260
+ disabled=True,
261
+ key=f"c_dis_{table}_{col_idx}_{col_name}",
262
+ label_visibility="collapsed",
263
+ help="Globally ignored",
264
+ )
265
+ else:
266
+ chk = st.checkbox(
267
+ "",
268
+ is_local,
269
+ key=f"c_{table}_{col_idx}_{col_name}",
270
+ label_visibility="collapsed",
271
+ help="Check to ignore this column",
272
+ )
273
+ if chk:
274
+ st.session_state.ignore_cols_per_table[table].add(col_name)
275
+ else:
276
+ st.session_state.ignore_cols_per_table[table].discard(col_name)
277
+
278
+ with c2:
279
+ name_display = f"~~{col_name}~~" if is_global or is_local else col_name
280
+ st.text(name_display)
281
+ with c3:
282
+ st.caption(col["type"][:25])
283
+ with c4:
284
+ ex_display = f"~~{ex_str[:80]}~~" if is_global or is_local else ex_str[:80]
285
+ st.caption(ex_display)
286
+
287
+ # Output
288
+ st.write("---")
289
+ st.write("### 📝 Config")
290
+
291
+ config = "audit_ignore_tables=[\n"
292
+
293
+ # Add fully ignored tables as strings
294
+ for t in sorted(st.session_state.ignore_tables):
295
+ config += f' "{t}",\n'
296
+
297
+ # Add tables with column-level ignores as dicts
298
+ for table, cols in sorted(st.session_state.ignore_cols_per_table.items()):
299
+ if cols and table not in st.session_state.ignore_tables:
300
+ config += f" {{'table': '{table}', 'columns': {json.dumps(sorted(list[Any](cols)))}}},\n"
301
+
302
+ config += "]\n"
303
+
304
+ if st.session_state.ignore_cols_global:
305
+ config += "\n# Global column filters (apply to ALL tables):\n"
306
+ config += "# These should be added to ignore_columns with '*' key if needed\n"
307
+ config += f"# ignore_columns = {{'*': {json.dumps(sorted(list[Any](st.session_state.ignore_cols_global)))}}}\n"
308
+
309
+ if config.count("\n") > 2:
310
+ st.code(config, language="python")
311
+ else:
312
+ st.info("Mark items to generate config")
313
+
314
+
315
+ if __name__ == "__main__":
316
+ main()