plato-sdk-v2 2.7.6__py3-none-any.whl → 2.7.8__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/__init__.py +5 -0
- plato/cli/agent.py +1209 -0
- plato/cli/audit_ui.py +316 -0
- plato/cli/chronos.py +817 -0
- plato/cli/main.py +193 -0
- plato/cli/pm.py +1204 -0
- plato/cli/proxy.py +222 -0
- plato/cli/sandbox.py +808 -0
- plato/cli/utils.py +200 -0
- plato/cli/verify.py +690 -0
- plato/cli/world.py +250 -0
- plato/v1/cli/pm.py +4 -1
- plato/v2/__init__.py +2 -0
- plato/v2/models.py +42 -0
- plato/v2/sync/__init__.py +6 -0
- plato/v2/sync/client.py +6 -3
- plato/v2/sync/sandbox.py +1462 -0
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.8.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.8.dist-info}/RECORD +21 -9
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.8.dist-info}/entry_points.txt +1 -1
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.8.dist-info}/WHEEL +0 -0
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()
|