anysite-cli 0.1.0__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.
Potentially problematic release.
This version of anysite-cli might be problematic. Click here for more details.
- anysite/__init__.py +4 -0
- anysite/__main__.py +6 -0
- anysite/api/__init__.py +21 -0
- anysite/api/client.py +271 -0
- anysite/api/errors.py +137 -0
- anysite/api/schemas.py +333 -0
- anysite/batch/__init__.py +1 -0
- anysite/batch/executor.py +176 -0
- anysite/batch/input.py +160 -0
- anysite/batch/rate_limiter.py +98 -0
- anysite/cli/__init__.py +1 -0
- anysite/cli/config.py +176 -0
- anysite/cli/executor.py +388 -0
- anysite/cli/options.py +249 -0
- anysite/config/__init__.py +11 -0
- anysite/config/paths.py +46 -0
- anysite/config/settings.py +187 -0
- anysite/dataset/__init__.py +37 -0
- anysite/dataset/analyzer.py +268 -0
- anysite/dataset/cli.py +644 -0
- anysite/dataset/collector.py +686 -0
- anysite/dataset/db_loader.py +248 -0
- anysite/dataset/errors.py +30 -0
- anysite/dataset/exporters.py +121 -0
- anysite/dataset/history.py +153 -0
- anysite/dataset/models.py +245 -0
- anysite/dataset/notifications.py +87 -0
- anysite/dataset/scheduler.py +107 -0
- anysite/dataset/storage.py +171 -0
- anysite/dataset/transformer.py +213 -0
- anysite/db/__init__.py +38 -0
- anysite/db/adapters/__init__.py +1 -0
- anysite/db/adapters/base.py +158 -0
- anysite/db/adapters/postgres.py +201 -0
- anysite/db/adapters/sqlite.py +183 -0
- anysite/db/cli.py +687 -0
- anysite/db/config.py +92 -0
- anysite/db/manager.py +166 -0
- anysite/db/operations/__init__.py +1 -0
- anysite/db/operations/insert.py +199 -0
- anysite/db/operations/query.py +43 -0
- anysite/db/schema/__init__.py +1 -0
- anysite/db/schema/inference.py +213 -0
- anysite/db/schema/types.py +71 -0
- anysite/db/utils/__init__.py +1 -0
- anysite/db/utils/sanitize.py +99 -0
- anysite/main.py +498 -0
- anysite/models/__init__.py +1 -0
- anysite/output/__init__.py +11 -0
- anysite/output/console.py +45 -0
- anysite/output/formatters.py +301 -0
- anysite/output/templates.py +76 -0
- anysite/py.typed +0 -0
- anysite/streaming/__init__.py +1 -0
- anysite/streaming/progress.py +121 -0
- anysite/streaming/writer.py +130 -0
- anysite/utils/__init__.py +1 -0
- anysite/utils/fields.py +242 -0
- anysite/utils/retry.py +109 -0
- anysite_cli-0.1.0.dist-info/METADATA +437 -0
- anysite_cli-0.1.0.dist-info/RECORD +64 -0
- anysite_cli-0.1.0.dist-info/WHEEL +4 -0
- anysite_cli-0.1.0.dist-info/entry_points.txt +2 -0
- anysite_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""SQLite database adapter using stdlib sqlite3."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from anysite.db.adapters.base import DatabaseAdapter
|
|
11
|
+
from anysite.db.config import ConnectionConfig, OnConflict
|
|
12
|
+
from anysite.db.utils.sanitize import sanitize_identifier, sanitize_table_name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SQLiteAdapter(DatabaseAdapter):
|
|
16
|
+
"""SQLite adapter using the stdlib sqlite3 module."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config: ConnectionConfig) -> None:
|
|
19
|
+
self.config = config
|
|
20
|
+
self.db_path = config.path or ":memory:"
|
|
21
|
+
self._conn: sqlite3.Connection | None = None
|
|
22
|
+
|
|
23
|
+
def connect(self) -> None:
|
|
24
|
+
if self._conn is not None:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
# Ensure parent directory exists for file-based databases
|
|
28
|
+
if self.db_path != ":memory:":
|
|
29
|
+
Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
self._conn = sqlite3.connect(self.db_path)
|
|
32
|
+
self._conn.row_factory = sqlite3.Row
|
|
33
|
+
|
|
34
|
+
# Enable WAL mode for better concurrent access
|
|
35
|
+
if self.db_path != ":memory:":
|
|
36
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
37
|
+
|
|
38
|
+
# Enable foreign keys
|
|
39
|
+
self._conn.execute("PRAGMA foreign_keys=ON")
|
|
40
|
+
|
|
41
|
+
def disconnect(self) -> None:
|
|
42
|
+
if self._conn is not None:
|
|
43
|
+
self._conn.close()
|
|
44
|
+
self._conn = None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def conn(self) -> sqlite3.Connection:
|
|
48
|
+
if self._conn is None:
|
|
49
|
+
raise RuntimeError("Not connected. Call connect() first or use as context manager.")
|
|
50
|
+
return self._conn
|
|
51
|
+
|
|
52
|
+
def execute(self, sql: str, params: tuple[Any, ...] | None = None) -> None:
|
|
53
|
+
self.conn.execute(sql, params or ())
|
|
54
|
+
self.conn.commit()
|
|
55
|
+
|
|
56
|
+
def fetch_one(self, sql: str, params: tuple[Any, ...] | None = None) -> dict[str, Any] | None:
|
|
57
|
+
cursor = self.conn.execute(sql, params or ())
|
|
58
|
+
row = cursor.fetchone()
|
|
59
|
+
if row is None:
|
|
60
|
+
return None
|
|
61
|
+
return dict(row)
|
|
62
|
+
|
|
63
|
+
def fetch_all(self, sql: str, params: tuple[Any, ...] | None = None) -> list[dict[str, Any]]:
|
|
64
|
+
cursor = self.conn.execute(sql, params or ())
|
|
65
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
66
|
+
|
|
67
|
+
def insert_batch(
|
|
68
|
+
self,
|
|
69
|
+
table: str,
|
|
70
|
+
rows: list[dict[str, Any]],
|
|
71
|
+
on_conflict: OnConflict = OnConflict.ERROR,
|
|
72
|
+
conflict_columns: list[str] | None = None,
|
|
73
|
+
) -> int:
|
|
74
|
+
if not rows:
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
safe_table = sanitize_table_name(table)
|
|
78
|
+
|
|
79
|
+
# Collect all column names from all rows
|
|
80
|
+
all_columns: list[str] = []
|
|
81
|
+
seen: set[str] = set()
|
|
82
|
+
for row in rows:
|
|
83
|
+
for col in row:
|
|
84
|
+
if col not in seen:
|
|
85
|
+
seen.add(col)
|
|
86
|
+
all_columns.append(col)
|
|
87
|
+
|
|
88
|
+
safe_columns = [sanitize_identifier(col) for col in all_columns]
|
|
89
|
+
placeholders = ", ".join("?" for _ in all_columns)
|
|
90
|
+
col_list = ", ".join(safe_columns)
|
|
91
|
+
|
|
92
|
+
# Build the INSERT statement based on conflict strategy
|
|
93
|
+
if on_conflict == OnConflict.IGNORE:
|
|
94
|
+
sql = f"INSERT OR IGNORE INTO {safe_table} ({col_list}) VALUES ({placeholders})"
|
|
95
|
+
elif on_conflict == OnConflict.REPLACE:
|
|
96
|
+
sql = f"INSERT OR REPLACE INTO {safe_table} ({col_list}) VALUES ({placeholders})"
|
|
97
|
+
elif on_conflict == OnConflict.UPDATE and conflict_columns:
|
|
98
|
+
safe_conflict = [sanitize_identifier(c) for c in conflict_columns]
|
|
99
|
+
conflict_list = ", ".join(safe_conflict)
|
|
100
|
+
update_cols = [c for c in safe_columns if c not in safe_conflict]
|
|
101
|
+
update_clause = ", ".join(f"{c} = excluded.{c}" for c in update_cols)
|
|
102
|
+
sql = (
|
|
103
|
+
f"INSERT INTO {safe_table} ({col_list}) VALUES ({placeholders}) "
|
|
104
|
+
f"ON CONFLICT ({conflict_list}) DO UPDATE SET {update_clause}"
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
sql = f"INSERT INTO {safe_table} ({col_list}) VALUES ({placeholders})"
|
|
108
|
+
|
|
109
|
+
# Prepare parameter rows, serializing complex types
|
|
110
|
+
param_rows: list[tuple[Any, ...]] = []
|
|
111
|
+
for row in rows:
|
|
112
|
+
values: list[Any] = []
|
|
113
|
+
for col in all_columns:
|
|
114
|
+
val = row.get(col)
|
|
115
|
+
if isinstance(val, (dict, list)):
|
|
116
|
+
val = json.dumps(val)
|
|
117
|
+
values.append(val)
|
|
118
|
+
param_rows.append(tuple(values))
|
|
119
|
+
|
|
120
|
+
cursor = self.conn.cursor()
|
|
121
|
+
cursor.executemany(sql, param_rows)
|
|
122
|
+
self.conn.commit()
|
|
123
|
+
return cursor.rowcount
|
|
124
|
+
|
|
125
|
+
def table_exists(self, table: str) -> bool:
|
|
126
|
+
row = self.fetch_one(
|
|
127
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
|
128
|
+
(table,),
|
|
129
|
+
)
|
|
130
|
+
return row is not None
|
|
131
|
+
|
|
132
|
+
def get_table_schema(self, table: str) -> list[dict[str, str]]:
|
|
133
|
+
safe_table = sanitize_table_name(table)
|
|
134
|
+
rows = self.fetch_all(f"PRAGMA table_info({safe_table})")
|
|
135
|
+
result: list[dict[str, str]] = []
|
|
136
|
+
for row in rows:
|
|
137
|
+
result.append({
|
|
138
|
+
"name": row["name"],
|
|
139
|
+
"type": row["type"],
|
|
140
|
+
"nullable": "NO" if row["notnull"] else "YES",
|
|
141
|
+
"primary_key": "YES" if row["pk"] else "NO",
|
|
142
|
+
})
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
def create_table(
|
|
146
|
+
self,
|
|
147
|
+
table: str,
|
|
148
|
+
columns: dict[str, str],
|
|
149
|
+
primary_key: str | None = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
safe_table = sanitize_table_name(table)
|
|
152
|
+
col_defs: list[str] = []
|
|
153
|
+
for col_name, col_type in columns.items():
|
|
154
|
+
safe_col = sanitize_identifier(col_name)
|
|
155
|
+
pk_suffix = " PRIMARY KEY" if col_name == primary_key else ""
|
|
156
|
+
col_defs.append(f"{safe_col} {col_type}{pk_suffix}")
|
|
157
|
+
|
|
158
|
+
cols_sql = ", ".join(col_defs)
|
|
159
|
+
sql = f"CREATE TABLE IF NOT EXISTS {safe_table} ({cols_sql})"
|
|
160
|
+
self.execute(sql)
|
|
161
|
+
|
|
162
|
+
def get_server_info(self) -> dict[str, str]:
|
|
163
|
+
return {
|
|
164
|
+
"type": "sqlite",
|
|
165
|
+
"version": sqlite3.sqlite_version,
|
|
166
|
+
"path": self.db_path,
|
|
167
|
+
"journal_mode": self._get_pragma("journal_mode"),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def _get_pragma(self, name: str) -> str:
|
|
171
|
+
row = self.fetch_one(f"PRAGMA {name}")
|
|
172
|
+
if row:
|
|
173
|
+
return str(list(row.values())[0])
|
|
174
|
+
return "unknown"
|
|
175
|
+
|
|
176
|
+
def _begin_transaction(self) -> None:
|
|
177
|
+
self.conn.execute("BEGIN")
|
|
178
|
+
|
|
179
|
+
def _commit_transaction(self) -> None:
|
|
180
|
+
self.conn.commit()
|
|
181
|
+
|
|
182
|
+
def _rollback_transaction(self) -> None:
|
|
183
|
+
self.conn.rollback()
|