sqlbench 0.1.4__tar.gz → 0.1.6__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.
- {sqlbench-0.1.4 → sqlbench-0.1.6}/PKG-INFO +4 -1
- {sqlbench-0.1.4 → sqlbench-0.1.6}/pyproject.toml +3 -1
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/adapters.py +130 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/app.py +2 -1
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/dialogs/connection_dialog.py +120 -5
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/version.py +1 -1
- {sqlbench-0.1.4 → sqlbench-0.1.6}/.github/workflows/pypi-publish.yml +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/.gitignore +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/Makefile +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/README.md +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/requirements.txt +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/__init__.py +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/__main__.py +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/database.py +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/dialogs/__init__.py +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/dialogs/regex_builder_dialog.py +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/launcher.py +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/resources/sqlbench.png +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/tabs/__init__.py +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/tabs/spool_tab.py +0 -0
- {sqlbench-0.1.4 → sqlbench-0.1.6}/sqlbench/tabs/sql_tab.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlbench
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: A multi-database SQL workbench with support for IBM i, MySQL, and PostgreSQL
|
|
5
5
|
Project-URL: Homepage, https://github.com/jpsteil/sqlbench
|
|
6
6
|
Project-URL: Repository, https://github.com/jpsteil/sqlbench
|
|
@@ -24,6 +24,7 @@ Classifier: Topic :: Database :: Front-Ends
|
|
|
24
24
|
Requires-Python: >=3.9
|
|
25
25
|
Requires-Dist: sqlparse>=0.5.0
|
|
26
26
|
Provides-Extra: all
|
|
27
|
+
Requires-Dist: ibm-db>=3.0.0; extra == 'all'
|
|
27
28
|
Requires-Dist: mysql-connector-python>=8.0.0; extra == 'all'
|
|
28
29
|
Requires-Dist: openpyxl>=3.1.0; extra == 'all'
|
|
29
30
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'all'
|
|
@@ -38,6 +39,8 @@ Requires-Dist: openpyxl>=3.1.0; extra == 'export'
|
|
|
38
39
|
Requires-Dist: reportlab>=4.0.0; extra == 'export'
|
|
39
40
|
Provides-Extra: ibmi
|
|
40
41
|
Requires-Dist: pyodbc>=4.0.0; extra == 'ibmi'
|
|
42
|
+
Provides-Extra: ibmi-db
|
|
43
|
+
Requires-Dist: ibm-db>=3.0.0; extra == 'ibmi-db'
|
|
41
44
|
Provides-Extra: mysql
|
|
42
45
|
Requires-Dist: mysql-connector-python>=8.0.0; extra == 'mysql'
|
|
43
46
|
Provides-Extra: postgresql
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlbench"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "A multi-database SQL workbench with support for IBM i, MySQL, and PostgreSQL"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -36,11 +36,13 @@ dependencies = [
|
|
|
36
36
|
mysql = ["mysql-connector-python>=8.0.0"]
|
|
37
37
|
postgresql = ["psycopg2-binary>=2.9.0"]
|
|
38
38
|
ibmi = ["pyodbc>=4.0.0"]
|
|
39
|
+
ibmi-db = ["ibm_db>=3.0.0"]
|
|
39
40
|
export = ["reportlab>=4.0.0", "openpyxl>=3.1.0"]
|
|
40
41
|
all = [
|
|
41
42
|
"mysql-connector-python>=8.0.0",
|
|
42
43
|
"psycopg2-binary>=2.9.0",
|
|
43
44
|
"pyodbc>=4.0.0",
|
|
45
|
+
"ibm_db>=3.0.0",
|
|
44
46
|
"reportlab>=4.0.0",
|
|
45
47
|
"openpyxl>=3.1.0",
|
|
46
48
|
]
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
"""Database adapters for different database types."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _setup_ibm_db_environment():
|
|
9
|
+
"""Set up environment variables for ibm_db if clidriver is installed."""
|
|
10
|
+
if os.environ.get("IBM_DB_HOME"):
|
|
11
|
+
return # Already configured
|
|
12
|
+
|
|
13
|
+
# Check common install locations
|
|
14
|
+
clidriver_paths = [
|
|
15
|
+
Path.home() / "db2drivers" / "clidriver",
|
|
16
|
+
Path("/opt/ibm/db2/clidriver"),
|
|
17
|
+
Path("/opt/clidriver"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
for cli_path in clidriver_paths:
|
|
21
|
+
if (cli_path / "lib").exists():
|
|
22
|
+
os.environ["IBM_DB_HOME"] = str(cli_path)
|
|
23
|
+
lib_path = os.environ.get("LD_LIBRARY_PATH", "")
|
|
24
|
+
os.environ["LD_LIBRARY_PATH"] = f"{cli_path}/lib:{lib_path}"
|
|
25
|
+
break
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Set up ibm_db environment before any imports
|
|
29
|
+
_setup_ibm_db_environment()
|
|
4
30
|
|
|
5
31
|
|
|
6
32
|
class DBAdapter(ABC):
|
|
@@ -175,6 +201,109 @@ class IBMiAdapter(DBAdapter):
|
|
|
175
201
|
"""
|
|
176
202
|
|
|
177
203
|
|
|
204
|
+
class IBMiDBAdapter(DBAdapter):
|
|
205
|
+
"""Adapter for IBM i (AS/400) via ibm_db (native CLI driver)."""
|
|
206
|
+
|
|
207
|
+
db_type = "ibmi_db"
|
|
208
|
+
display_name = "IBM i (ibm_db)"
|
|
209
|
+
default_port = 446 # DRDA port
|
|
210
|
+
requires_database = False
|
|
211
|
+
supports_spool = True
|
|
212
|
+
required_module = "ibm_db"
|
|
213
|
+
install_hint = "pip install sqlbench[ibmi-db]"
|
|
214
|
+
|
|
215
|
+
def connect(self, host, user, password, port=None, database=None):
|
|
216
|
+
import ibm_db_dbi
|
|
217
|
+
|
|
218
|
+
# Build connection string for IBM i
|
|
219
|
+
# DATABASE can be *LOCAL or the RDB name
|
|
220
|
+
db_name = database if database else "*LOCAL"
|
|
221
|
+
port_num = port if port else 446
|
|
222
|
+
|
|
223
|
+
conn_str = (
|
|
224
|
+
f"DATABASE={db_name};"
|
|
225
|
+
f"HOSTNAME={host};"
|
|
226
|
+
f"PORT={port_num};"
|
|
227
|
+
f"PROTOCOL=TCPIP;"
|
|
228
|
+
f"UID={user};"
|
|
229
|
+
f"PWD={password};"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# ibm_db_dbi provides DB-API 2.0 compliant interface
|
|
233
|
+
return ibm_db_dbi.connect(conn_str, "", "")
|
|
234
|
+
|
|
235
|
+
def get_version(self, conn):
|
|
236
|
+
try:
|
|
237
|
+
cursor = conn.cursor()
|
|
238
|
+
cursor.execute("SELECT OS_VERSION, OS_RELEASE FROM SYSIBMADM.ENV_SYS_INFO")
|
|
239
|
+
row = cursor.fetchone()
|
|
240
|
+
cursor.close()
|
|
241
|
+
if row:
|
|
242
|
+
return f"{row[0]}.{row[1]}"
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
def add_pagination(self, sql, limit, offset=0):
|
|
248
|
+
"""IBM i uses OFFSET/FETCH syntax."""
|
|
249
|
+
sql_stripped = sql.strip()
|
|
250
|
+
while sql_stripped.endswith(';'):
|
|
251
|
+
sql_stripped = sql_stripped[:-1].strip()
|
|
252
|
+
|
|
253
|
+
if offset > 0:
|
|
254
|
+
return f"{sql_stripped} OFFSET {offset} ROWS FETCH FIRST {limit} ROWS ONLY"
|
|
255
|
+
return f"{sql_stripped} FETCH FIRST {limit} ROWS ONLY"
|
|
256
|
+
|
|
257
|
+
def get_select_limit_query(self, table_ref, limit):
|
|
258
|
+
"""Get a SELECT query with row limit for IBM i."""
|
|
259
|
+
return f"SELECT * FROM {table_ref} FETCH FIRST {limit} ROWS ONLY"
|
|
260
|
+
|
|
261
|
+
def get_version_query(self):
|
|
262
|
+
"""Get the SQL to retrieve IBM i version."""
|
|
263
|
+
return "SELECT OS_VERSION || '.' || OS_RELEASE FROM SYSIBMADM.ENV_SYS_INFO"
|
|
264
|
+
|
|
265
|
+
def get_columns_query(self, tables):
|
|
266
|
+
if not tables:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
table_conditions = []
|
|
270
|
+
for table in tables:
|
|
271
|
+
if '.' in table:
|
|
272
|
+
schema, tbl = table.split('.', 1)
|
|
273
|
+
table_conditions.append(
|
|
274
|
+
f"(TABLE_SCHEMA = '{schema.upper()}' AND TABLE_NAME = '{tbl.upper()}')"
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
table_conditions.append(f"TABLE_NAME = '{table.upper()}'")
|
|
278
|
+
|
|
279
|
+
where_clause = " OR ".join(table_conditions)
|
|
280
|
+
return f"""
|
|
281
|
+
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, LENGTH, NUMERIC_SCALE
|
|
282
|
+
FROM QSYS2.SYSCOLUMNS
|
|
283
|
+
WHERE {where_clause}
|
|
284
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
def get_tables_query(self):
|
|
288
|
+
"""Get tables from IBM i - returns schema, table_name, table_type."""
|
|
289
|
+
return """
|
|
290
|
+
SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE
|
|
291
|
+
FROM QSYS2.SYSTABLES
|
|
292
|
+
WHERE TABLE_TYPE IN ('T', 'P', 'V')
|
|
293
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
def is_numeric_type(self, type_code):
|
|
297
|
+
"""Check if a type_code represents a numeric type for ibm_db."""
|
|
298
|
+
# ibm_db uses type codes similar to CLI
|
|
299
|
+
# Check for Python numeric types as fallback
|
|
300
|
+
from decimal import Decimal
|
|
301
|
+
if isinstance(type_code, type):
|
|
302
|
+
return type_code in (int, float, Decimal)
|
|
303
|
+
# ibm_db type codes for numeric: various integer constants
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
|
|
178
307
|
class MySQLAdapter(DBAdapter):
|
|
179
308
|
"""Adapter for MySQL."""
|
|
180
309
|
|
|
@@ -384,6 +513,7 @@ class PostgreSQLAdapter(DBAdapter):
|
|
|
384
513
|
# Registry of available adapters
|
|
385
514
|
ADAPTERS = {
|
|
386
515
|
'ibmi': IBMiAdapter,
|
|
516
|
+
'ibmi_db': IBMiDBAdapter,
|
|
387
517
|
'mysql': MySQLAdapter,
|
|
388
518
|
'postgresql': PostgreSQLAdapter,
|
|
389
519
|
}
|
|
@@ -7,6 +7,7 @@ from tkinter import ttk
|
|
|
7
7
|
|
|
8
8
|
from sqlbench.database import Database
|
|
9
9
|
from sqlbench.adapters import get_adapter, ADAPTERS
|
|
10
|
+
from sqlbench.version import __version__
|
|
10
11
|
from sqlbench.tabs.sql_tab import SQLTab
|
|
11
12
|
from sqlbench.tabs.spool_tab import SpoolTab
|
|
12
13
|
from sqlbench.dialogs.connection_dialog import ConnectionDialog
|
|
@@ -18,7 +19,7 @@ class SQLBenchApp:
|
|
|
18
19
|
# Set className for proper window manager integration (Linux/X11)
|
|
19
20
|
# This makes the app show as "SQLBench" in window lists and match the .desktop file
|
|
20
21
|
self.root = tk.Tk(className="sqlbench")
|
|
21
|
-
self.root.title("SQLBench")
|
|
22
|
+
self.root.title(f"SQLBench v{__version__}")
|
|
22
23
|
|
|
23
24
|
# Set window icon
|
|
24
25
|
self._set_window_icon()
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Connection management dialog."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import tkinter as tk
|
|
4
5
|
from tkinter import ttk, messagebox
|
|
6
|
+
from pathlib import Path
|
|
5
7
|
import threading
|
|
6
8
|
|
|
7
9
|
from sqlbench.adapters import get_adapter_choices, get_adapter, get_unavailable_adapters, ADAPTERS
|
|
@@ -204,7 +206,7 @@ class ConnectionDialog:
|
|
|
204
206
|
for conn in self._connections:
|
|
205
207
|
# Show type indicator
|
|
206
208
|
db_type = conn.get("db_type", "ibmi")
|
|
207
|
-
type_indicator = {"ibmi": "[i]", "mysql": "[M]", "postgresql": "[P]"}.get(db_type, "[?]")
|
|
209
|
+
type_indicator = {"ibmi": "[i]", "ibmi_db": "[I]", "mysql": "[M]", "postgresql": "[P]"}.get(db_type, "[?]")
|
|
208
210
|
# Mark unavailable connections
|
|
209
211
|
if db_type not in available_types:
|
|
210
212
|
type_indicator = "[!]"
|
|
@@ -430,6 +432,97 @@ class ConnectionDialog:
|
|
|
430
432
|
status_text.config(state=tk.DISABLED)
|
|
431
433
|
dialog.update()
|
|
432
434
|
|
|
435
|
+
def install_clidriver(log_status):
|
|
436
|
+
"""Download and install IBM Db2 clidriver for ibm_db."""
|
|
437
|
+
import platform
|
|
438
|
+
import tarfile
|
|
439
|
+
import urllib.request
|
|
440
|
+
from pathlib import Path
|
|
441
|
+
|
|
442
|
+
# Determine platform and download URL
|
|
443
|
+
system = platform.system().lower()
|
|
444
|
+
machine = platform.machine().lower()
|
|
445
|
+
|
|
446
|
+
if system == "linux" and machine in ("x86_64", "amd64"):
|
|
447
|
+
url = "https://public.dhe.ibm.com/ibmdl/export/pub/software/data/db2/drivers/odbc_cli/linuxx64_odbc_cli.tar.gz"
|
|
448
|
+
elif system == "linux" and machine in ("aarch64", "arm64"):
|
|
449
|
+
url = "https://public.dhe.ibm.com/ibmdl/export/pub/software/data/db2/drivers/odbc_cli/linuxarm64_odbc_cli.tar.gz"
|
|
450
|
+
elif system == "darwin" and machine in ("x86_64", "amd64"):
|
|
451
|
+
url = "https://public.dhe.ibm.com/ibmdl/export/pub/software/data/db2/drivers/odbc_cli/macos64_odbc_cli.tar.gz"
|
|
452
|
+
elif system == "darwin" and machine == "arm64":
|
|
453
|
+
# M1/M2 Mac - use x86_64 version with Rosetta or native if available
|
|
454
|
+
url = "https://public.dhe.ibm.com/ibmdl/export/pub/software/data/db2/drivers/odbc_cli/macos64_odbc_cli.tar.gz"
|
|
455
|
+
elif system == "windows":
|
|
456
|
+
log_status(" Windows: Please download clidriver manually from IBM")
|
|
457
|
+
return False
|
|
458
|
+
else:
|
|
459
|
+
log_status(f" Unsupported platform: {system}/{machine}")
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
# Set up paths
|
|
463
|
+
home = Path.home()
|
|
464
|
+
driver_dir = home / "db2drivers"
|
|
465
|
+
clidriver_path = driver_dir / "clidriver"
|
|
466
|
+
tar_file = driver_dir / "odbc_cli.tar.gz"
|
|
467
|
+
|
|
468
|
+
# Check if already installed
|
|
469
|
+
if (clidriver_path / "lib").exists():
|
|
470
|
+
log_status(" clidriver already installed")
|
|
471
|
+
return True
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
# Create directory
|
|
475
|
+
driver_dir.mkdir(parents=True, exist_ok=True)
|
|
476
|
+
|
|
477
|
+
# Download
|
|
478
|
+
log_status(" Downloading clidriver (~100MB)...")
|
|
479
|
+
urllib.request.urlretrieve(url, tar_file)
|
|
480
|
+
|
|
481
|
+
# Extract
|
|
482
|
+
log_status(" Extracting...")
|
|
483
|
+
with tarfile.open(tar_file, "r:gz") as tar:
|
|
484
|
+
tar.extractall(path=driver_dir)
|
|
485
|
+
|
|
486
|
+
# Clean up tar file
|
|
487
|
+
tar_file.unlink()
|
|
488
|
+
|
|
489
|
+
# Set environment variable hint
|
|
490
|
+
log_status(" clidriver installed to ~/db2drivers/clidriver")
|
|
491
|
+
|
|
492
|
+
# Update environment for current process
|
|
493
|
+
import os
|
|
494
|
+
cli_path = str(clidriver_path)
|
|
495
|
+
os.environ["IBM_DB_HOME"] = cli_path
|
|
496
|
+
lib_path = os.environ.get("LD_LIBRARY_PATH", "")
|
|
497
|
+
os.environ["LD_LIBRARY_PATH"] = f"{cli_path}/lib:{lib_path}"
|
|
498
|
+
|
|
499
|
+
# Add to shell config
|
|
500
|
+
shell_config = home / ".bashrc"
|
|
501
|
+
if not shell_config.exists():
|
|
502
|
+
shell_config = home / ".profile"
|
|
503
|
+
|
|
504
|
+
config_lines = [
|
|
505
|
+
f'\n# IBM Db2 clidriver (added by SQLBench)',
|
|
506
|
+
f'export IBM_DB_HOME=~/db2drivers/clidriver',
|
|
507
|
+
f'export LD_LIBRARY_PATH=$IBM_DB_HOME/lib:$LD_LIBRARY_PATH',
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
# Check if already in config
|
|
511
|
+
try:
|
|
512
|
+
existing = shell_config.read_text() if shell_config.exists() else ""
|
|
513
|
+
if "IBM_DB_HOME" not in existing:
|
|
514
|
+
with open(shell_config, "a") as f:
|
|
515
|
+
f.write("\n".join(config_lines) + "\n")
|
|
516
|
+
log_status(f" Added environment vars to {shell_config.name}")
|
|
517
|
+
except Exception:
|
|
518
|
+
log_status(" Note: Add IBM_DB_HOME to your shell config manually")
|
|
519
|
+
|
|
520
|
+
return True
|
|
521
|
+
|
|
522
|
+
except Exception as e:
|
|
523
|
+
log_status(f" clidriver install failed: {e}")
|
|
524
|
+
return False
|
|
525
|
+
|
|
433
526
|
def do_install():
|
|
434
527
|
install_btn.config(state=tk.DISABLED)
|
|
435
528
|
selected = [db_type for db_type, var in check_vars.items() if var.get()]
|
|
@@ -440,13 +533,35 @@ class ConnectionDialog:
|
|
|
440
533
|
|
|
441
534
|
python = sys.executable
|
|
442
535
|
for db_type in selected:
|
|
443
|
-
extra = {"ibmi": "ibmi", "mysql": "mysql", "postgresql": "postgresql"}.get(db_type)
|
|
536
|
+
extra = {"ibmi": "ibmi", "ibmi_db": "ibmi-db", "mysql": "mysql", "postgresql": "postgresql"}.get(db_type)
|
|
444
537
|
if extra:
|
|
445
|
-
|
|
538
|
+
# Special handling for ibm_db - need clidriver first
|
|
539
|
+
if db_type == "ibmi_db":
|
|
540
|
+
log_status("Installing IBM clidriver...")
|
|
541
|
+
if not install_clidriver(log_status):
|
|
542
|
+
log_status(" Skipping ibm_db (clidriver required)")
|
|
543
|
+
continue
|
|
544
|
+
|
|
545
|
+
# Map db_type to actual pip package
|
|
546
|
+
packages = {
|
|
547
|
+
"ibmi": "pyodbc",
|
|
548
|
+
"ibmi_db": "ibm_db",
|
|
549
|
+
"mysql": "mysql-connector-python",
|
|
550
|
+
"postgresql": "psycopg2-binary",
|
|
551
|
+
}
|
|
552
|
+
package = packages.get(db_type, extra)
|
|
553
|
+
log_status(f"Installing {package}...")
|
|
446
554
|
try:
|
|
555
|
+
# Set up environment - inherit current env and add IBM_DB_HOME if needed
|
|
556
|
+
env = os.environ.copy()
|
|
557
|
+
if db_type == "ibmi_db":
|
|
558
|
+
cli_path = Path.home() / "db2drivers" / "clidriver"
|
|
559
|
+
if cli_path.exists():
|
|
560
|
+
env["IBM_DB_HOME"] = str(cli_path)
|
|
561
|
+
|
|
447
562
|
result = subprocess.run(
|
|
448
|
-
[python, "-m", "pip", "install",
|
|
449
|
-
capture_output=True, text=True, timeout=
|
|
563
|
+
[python, "-m", "pip", "install", package],
|
|
564
|
+
capture_output=True, text=True, timeout=180, env=env
|
|
450
565
|
)
|
|
451
566
|
if result.returncode == 0:
|
|
452
567
|
log_status(f" {extra}: OK")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|