pyflashdb 1.0.1__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.
@@ -0,0 +1,226 @@
1
+ """
2
+ PostgreSQL adapter for Flash.
3
+ Requires: pip install psycopg2-binary
4
+ """
5
+
6
+ from typing import Any, Dict, List, Optional
7
+ from ..filters import filters_to_sql, build_update_sql, python_type_to_sql
8
+ from ..exceptions import ConnectionError, QueryError, TransactionError
9
+
10
+
11
+ class PostgreSQLAdapter:
12
+ """
13
+ PostgreSQL backend adapter for FlashDB.
14
+ Uses psycopg2 under the hood.
15
+ """
16
+
17
+ def __init__(self, config: dict):
18
+ self.config = config
19
+ self.conn = None
20
+ self.cursor = None
21
+ self._in_transaction = False
22
+ self._connect()
23
+
24
+ def _connect(self):
25
+ try:
26
+ import psycopg2
27
+ import psycopg2.extras
28
+ self.conn = psycopg2.connect(
29
+ host=self.config.get("host", "localhost"),
30
+ port=self.config.get("port", 5432),
31
+ user=self.config.get("user", "postgres"),
32
+ password=self.config.get("password", ""),
33
+ dbname=self.config.get("database"),
34
+ )
35
+ self.conn.autocommit = True
36
+ self.cursor = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
37
+ except ImportError:
38
+ raise ConnectionError(
39
+ "psycopg2 is not installed. Run: pip install psycopg2-binary"
40
+ )
41
+ except Exception as e:
42
+ raise ConnectionError(f"PostgreSQL connection failed: {e}")
43
+
44
+ def _execute(self, sql: str, params: list = None) -> Any:
45
+ # psycopg2 uses %s but doesn't need None as []
46
+ try:
47
+ self.cursor.execute(sql, params if params else None)
48
+ return self.cursor
49
+ except Exception as e:
50
+ if not self._in_transaction:
51
+ self.conn.rollback()
52
+ raise QueryError(f"PostgreSQL query failed: {e}\nSQL: {sql}\nParams: {params}")
53
+
54
+ def _quote(self, name: str) -> str:
55
+ return f'"{name}"'
56
+
57
+ # ── Core CRUD ──────────────────────────────────────────────────────────
58
+
59
+ def all(self, table: str) -> List[dict]:
60
+ self._execute(f"SELECT * FROM {self._quote(table)}")
61
+ return [dict(row) for row in self.cursor.fetchall()]
62
+
63
+ def select(self, table: str, fields: List[str] = None, filters: dict = None,
64
+ limit: int = None, offset: int = None, order_by: str = None) -> List[dict]:
65
+ cols = ", ".join(self._quote(f) for f in fields) if fields else "*"
66
+ sql = f"SELECT {cols} FROM {self._quote(table)}"
67
+ params = []
68
+
69
+ if filters:
70
+ clause, params = filters_to_sql(filters)
71
+ sql += f" WHERE {clause}"
72
+
73
+ if order_by:
74
+ sql += f" ORDER BY {order_by}"
75
+
76
+ if limit is not None:
77
+ sql += f" LIMIT {int(limit)}"
78
+ if offset is not None:
79
+ sql += f" OFFSET {int(offset)}"
80
+
81
+ self._execute(sql, params)
82
+ return [dict(row) for row in self.cursor.fetchall()]
83
+
84
+ def add(self, table: str, data: dict) -> Any:
85
+ fields = ", ".join(self._quote(k) for k in data.keys())
86
+ placeholders = ", ".join(["%s"] * len(data))
87
+ sql = f"INSERT INTO {self._quote(table)} ({fields}) VALUES ({placeholders}) RETURNING *"
88
+ self._execute(sql, list(data.values()))
89
+ row = self.cursor.fetchone()
90
+ return dict(row) if row else None
91
+
92
+ def bulk_insert(self, table: str, records: List[dict]) -> int:
93
+ if not records:
94
+ return 0
95
+ fields = ", ".join(self._quote(k) for k in records[0].keys())
96
+ placeholders = ", ".join(["%s"] * len(records[0]))
97
+ sql = f"INSERT INTO {self._quote(table)} ({fields}) VALUES ({placeholders})"
98
+ rows = [list(r.values()) for r in records]
99
+ try:
100
+ self.cursor.executemany(sql, rows)
101
+ return self.cursor.rowcount
102
+ except Exception as e:
103
+ raise QueryError(f"PostgreSQL bulk insert failed: {e}")
104
+
105
+ def update(self, table: str, filters: dict, data: dict) -> int:
106
+ set_clause, set_params = build_update_sql(data)
107
+ where_clause, where_params = filters_to_sql(filters)
108
+ sql = f"UPDATE {self._quote(table)} SET {set_clause}"
109
+ if where_clause:
110
+ sql += f" WHERE {where_clause}"
111
+ self._execute(sql, set_params + where_params)
112
+ return self.cursor.rowcount
113
+
114
+ def delete(self, table: str, filters: dict = None) -> int:
115
+ sql = f"DELETE FROM {self._quote(table)}"
116
+ params = []
117
+ if filters:
118
+ clause, params = filters_to_sql(filters)
119
+ sql += f" WHERE {clause}"
120
+ self._execute(sql, params)
121
+ return self.cursor.rowcount
122
+
123
+ # ── Schema Operations ──────────────────────────────────────────────────
124
+
125
+ def create_table(self, table: str, schema: dict, primary_key: str = "id") -> bool:
126
+ col_defs = []
127
+ has_pk = False
128
+ for col, typ in schema.items():
129
+ sql_type = python_type_to_sql(typ, "postgres")
130
+ if col == primary_key:
131
+ col_defs.append(f"{self._quote(col)} SERIAL PRIMARY KEY")
132
+ has_pk = True
133
+ else:
134
+ col_defs.append(f"{self._quote(col)} {sql_type}")
135
+ if not has_pk:
136
+ col_defs.insert(0, f"{self._quote(primary_key)} SERIAL PRIMARY KEY")
137
+
138
+ sql = f"CREATE TABLE IF NOT EXISTS {self._quote(table)} ({', '.join(col_defs)})"
139
+ self._execute(sql)
140
+ return True
141
+
142
+ def drop_table(self, table: str) -> bool:
143
+ self._execute(f"DROP TABLE IF EXISTS {self._quote(table)}")
144
+ return True
145
+
146
+ def truncate(self, table: str) -> bool:
147
+ self._execute(f"TRUNCATE TABLE {self._quote(table)} RESTART IDENTITY CASCADE")
148
+ return True
149
+
150
+ def show_tables(self) -> List[str]:
151
+ sql = """
152
+ SELECT table_name FROM information_schema.tables
153
+ WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
154
+ """
155
+ self._execute(sql)
156
+ return [row["table_name"] for row in self.cursor.fetchall()]
157
+
158
+ def describe(self, table: str) -> List[dict]:
159
+ sql = """
160
+ SELECT column_name, data_type, is_nullable, column_default
161
+ FROM information_schema.columns
162
+ WHERE table_name = %s
163
+ ORDER BY ordinal_position
164
+ """
165
+ self._execute(sql, [table])
166
+ return [dict(row) for row in self.cursor.fetchall()]
167
+
168
+ # ── Transactions ───────────────────────────────────────────────────────
169
+
170
+ def begin(self):
171
+ self.conn.autocommit = False
172
+ self._in_transaction = True
173
+
174
+ def commit(self):
175
+ try:
176
+ self.conn.commit()
177
+ except Exception as e:
178
+ raise TransactionError(f"Commit failed: {e}")
179
+ finally:
180
+ self.conn.autocommit = True
181
+ self._in_transaction = False
182
+
183
+ def rollback(self):
184
+ try:
185
+ self.conn.rollback()
186
+ except Exception as e:
187
+ raise TransactionError(f"Rollback failed: {e}")
188
+ finally:
189
+ self.conn.autocommit = True
190
+ self._in_transaction = False
191
+
192
+ # ── Raw Query ──────────────────────────────────────────────────────────
193
+
194
+ def raw(self, sql: str, params: list = None) -> Any:
195
+ self._execute(sql, params)
196
+ try:
197
+ return [dict(row) for row in self.cursor.fetchall()]
198
+ except Exception:
199
+ return self.cursor.rowcount
200
+
201
+ # ── Count ──────────────────────────────────────────────────────────────
202
+
203
+ def count(self, table: str, filters: dict = None) -> int:
204
+ sql = f"SELECT COUNT(*) as cnt FROM {self._quote(table)}"
205
+ params = []
206
+ if filters:
207
+ clause, params = filters_to_sql(filters)
208
+ sql += f" WHERE {clause}"
209
+ self._execute(sql, params)
210
+ row = self.cursor.fetchone()
211
+ return row["cnt"] if row else 0
212
+
213
+ # ── Lifecycle ──────────────────────────────────────────────────────────
214
+
215
+ def close(self):
216
+ if self.cursor:
217
+ self.cursor.close()
218
+ if self.conn:
219
+ self.conn.close()
220
+
221
+ def ping(self) -> bool:
222
+ try:
223
+ self._execute("SELECT 1")
224
+ return True
225
+ except Exception:
226
+ return False
flash/core.py ADDED
@@ -0,0 +1,306 @@
1
+ """
2
+ Flash Core — FlashDB unified database interface.
3
+
4
+ Usage:
5
+ from flash import FlashDB
6
+
7
+ flash = FlashDB("mysql", config)
8
+ flash.add("users", {"name": "John", "age": 25})
9
+ flash.all("users")
10
+ """
11
+
12
+ from typing import Any, Dict, List, Optional, Callable
13
+ from .exceptions import UnsupportedDatabaseError, FlashError
14
+ from .triggers import make_trigger_mixin
15
+
16
+
17
+ class FlashDB(make_trigger_mixin()):
18
+ """
19
+ FlashDB — a unified, trigger-aware database interface.
20
+ Supports MySQL, PostgreSQL, and MongoDB with a single API.
21
+
22
+ Args:
23
+ db_type (str): One of 'mysql', 'postgres', 'mongodb'
24
+ config (dict): Connection configuration dict.
25
+
26
+ Example:
27
+ flash = FlashDB("mysql", {
28
+ "host": "localhost",
29
+ "user": "root",
30
+ "password": "1234",
31
+ "database": "mydb"
32
+ })
33
+ """
34
+
35
+ SUPPORTED = ("mysql", "postgres", "postgresql", "mongodb", "mongo")
36
+
37
+ def __init__(self, db_type: str, config: dict):
38
+ self.db_type = db_type.lower().strip()
39
+ self.config = config
40
+ self._init_triggers()
41
+ self._adapter = self._load_adapter()
42
+
43
+ def _load_adapter(self):
44
+ if self.db_type == "mysql":
45
+ from .adapters.mysql_adapter import MySQLAdapter
46
+ return MySQLAdapter(self.config)
47
+
48
+ elif self.db_type in ("postgres", "postgresql"):
49
+ from .adapters.postgres_adapter import PostgreSQLAdapter
50
+ return PostgreSQLAdapter(self.config)
51
+
52
+ elif self.db_type in ("mongodb", "mongo"):
53
+ from .adapters.mongo_adapter import MongoAdapter
54
+ return MongoAdapter(self.config)
55
+
56
+ else:
57
+ raise UnsupportedDatabaseError(
58
+ f"'{self.db_type}' is not supported. Choose from: mysql, postgres, mongodb"
59
+ )
60
+
61
+ # ── Read Operations ────────────────────────────────────────────────────
62
+
63
+ def all(self, table: str) -> List[dict]:
64
+ """Fetch all records from a table/collection."""
65
+ self._triggers.fire("before", "select", table, {})
66
+ result = self._adapter.all(table)
67
+ self._triggers.fire("after", "select", table, {}, result)
68
+ return result
69
+
70
+ def select(
71
+ self,
72
+ table: str,
73
+ fields: List[str] = None,
74
+ filters: dict = None,
75
+ limit: int = None,
76
+ offset: int = None,
77
+ order_by: str = None,
78
+ ) -> List[dict]:
79
+ """
80
+ Select records with optional field projection, filters, sorting, and pagination.
81
+
82
+ Args:
83
+ table: Table/collection name.
84
+ fields: List of field names to return.
85
+ filters: Flash filter dict (e.g. {"age": {">": 18}}).
86
+ limit: Maximum number of records.
87
+ offset: Number of records to skip.
88
+ order_by: Field name to sort by (prefix '-' for descending in MongoDB).
89
+
90
+ Example:
91
+ flash.select("users", fields=["name", "age"], filters={"age": {">": 18}}, limit=10)
92
+ """
93
+ payload = {"fields": fields, "filters": filters}
94
+ self._triggers.fire("before", "select", table, payload)
95
+ result = self._adapter.select(
96
+ table, fields=fields, filters=filters,
97
+ limit=limit, offset=offset, order_by=order_by
98
+ )
99
+ self._triggers.fire("after", "select", table, payload, result)
100
+ return result
101
+
102
+ def where(self, table: str, filters: dict, fields: List[str] = None) -> List[dict]:
103
+ """
104
+ Shorthand for filtered select.
105
+
106
+ Example:
107
+ flash.where("users", {"age": {">": 18}})
108
+ flash.where("users", {"name": "John"})
109
+ """
110
+ return self.select(table, fields=fields, filters=filters)
111
+
112
+ def find_one(self, table: str, filters: dict) -> Optional[dict]:
113
+ """Return the first matching record, or None."""
114
+ results = self.select(table, filters=filters, limit=1)
115
+ return results[0] if results else None
116
+
117
+ def count(self, table: str, filters: dict = None) -> int:
118
+ """Count records in a table/collection, optionally filtered."""
119
+ return self._adapter.count(table, filters)
120
+
121
+ def limit(self, table: str, n: int) -> List[dict]:
122
+ """Fetch the first N records."""
123
+ return self.select(table, limit=n)
124
+
125
+ def paginate(self, table: str, page: int = 1, size: int = 10) -> dict:
126
+ """
127
+ Paginate records.
128
+
129
+ Returns:
130
+ {
131
+ "data": [...],
132
+ "page": 1,
133
+ "size": 10,
134
+ "total": 42
135
+ }
136
+ """
137
+ offset = (page - 1) * size
138
+ data = self.select(table, limit=size, offset=offset)
139
+ total = self.count(table)
140
+ return {"data": data, "page": page, "size": size, "total": total}
141
+
142
+ # ── Write Operations ───────────────────────────────────────────────────
143
+
144
+ def add(self, table: str, data: dict) -> Any:
145
+ """
146
+ Insert a single record.
147
+
148
+ Returns:
149
+ Inserted ID (int for SQL, str for MongoDB).
150
+
151
+ Example:
152
+ flash.add("users", {"name": "John", "age": 25})
153
+ """
154
+ self._triggers.fire("before", "insert", table, data)
155
+ result = self._adapter.add(table, data)
156
+ self._triggers.fire("after", "insert", table, data, result)
157
+ return result
158
+
159
+ def bulk_insert(self, table: str, records: List[dict]) -> int:
160
+ """
161
+ Insert multiple records at once.
162
+
163
+ Returns:
164
+ Number of inserted records.
165
+
166
+ Example:
167
+ flash.bulk_insert("users", [{"name": "A"}, {"name": "B"}])
168
+ """
169
+ self._triggers.fire("before", "insert", table, records)
170
+ result = self._adapter.bulk_insert(table, records)
171
+ self._triggers.fire("after", "insert", table, records, result)
172
+ return result
173
+
174
+ def update(self, table: str, filters: dict, data: dict) -> int:
175
+ """
176
+ Update records matching filters with new data.
177
+
178
+ Returns:
179
+ Number of updated records.
180
+
181
+ Example:
182
+ flash.update("users", {"name": "John"}, {"age": 26})
183
+ """
184
+ payload = {"filters": filters, "data": data}
185
+ self._triggers.fire("before", "update", table, payload)
186
+ result = self._adapter.update(table, filters, data)
187
+ self._triggers.fire("after", "update", table, payload, result)
188
+ return result
189
+
190
+ def delete(self, table: str, filters: dict = None) -> int:
191
+ """
192
+ Delete records matching filters. If no filters given, deletes ALL records.
193
+
194
+ Returns:
195
+ Number of deleted records.
196
+
197
+ Example:
198
+ flash.delete("users", {"name": "John"})
199
+ """
200
+ self._triggers.fire("before", "delete", table, filters or {})
201
+ result = self._adapter.delete(table, filters)
202
+ self._triggers.fire("after", "delete", table, filters or {}, result)
203
+ return result
204
+
205
+ # ── Schema Operations ──────────────────────────────────────────────────
206
+
207
+ def create_table(self, table: str, schema: dict, primary_key: str = "id") -> bool:
208
+ """
209
+ Create a table/collection with the given schema.
210
+
211
+ Args:
212
+ table: Table name.
213
+ schema: Dict of {column_name: type_string}.
214
+ primary_key: Name of the primary key column (SQL only).
215
+
216
+ Example:
217
+ flash.create_table("users", {
218
+ "id": "int",
219
+ "name": "str",
220
+ "email": "str",
221
+ "age": "int"
222
+ })
223
+ """
224
+ return self._adapter.create_table(table, schema, primary_key)
225
+
226
+ def drop_table(self, table: str) -> bool:
227
+ """Drop a table/collection entirely."""
228
+ return self._adapter.drop_table(table)
229
+
230
+ def truncate(self, table: str) -> bool:
231
+ """Remove all records from a table/collection without dropping it."""
232
+ return self._adapter.truncate(table)
233
+
234
+ def show_tables(self) -> List[str]:
235
+ """List all tables/collections in the database."""
236
+ return self._adapter.show_tables()
237
+
238
+ def describe(self, table: str):
239
+ """Return schema/column information for a table/collection."""
240
+ return self._adapter.describe(table)
241
+
242
+ # ── Transactions ───────────────────────────────────────────────────────
243
+
244
+ def begin(self):
245
+ """Begin a transaction (SQL) or session (MongoDB 4+ replica set)."""
246
+ self._adapter.begin()
247
+
248
+ def commit(self):
249
+ """Commit the current transaction."""
250
+ self._adapter.commit()
251
+
252
+ def rollback(self):
253
+ """Roll back the current transaction."""
254
+ self._adapter.rollback()
255
+
256
+ # ── Raw Query Escape Hatch ─────────────────────────────────────────────
257
+
258
+ def raw(self, query, params: list = None, table: str = None) -> Any:
259
+ """
260
+ Execute a raw query/command.
261
+
262
+ SQL: flash.raw("SELECT * FROM users WHERE age > %s", [18])
263
+ Mongo: flash.raw({"name": "John"}, table="users")
264
+ """
265
+ return self._adapter.raw(query, params) if params is not None else self._adapter.raw(query)
266
+
267
+ # ── MongoDB-Specific ───────────────────────────────────────────────────
268
+
269
+ def aggregate(self, table: str, pipeline: list) -> List[dict]:
270
+ """
271
+ Run a MongoDB aggregation pipeline (MongoDB only).
272
+
273
+ Example:
274
+ flash.aggregate("orders", [
275
+ {"$group": {"_id": "$user_id", "total": {"$sum": "$amount"}}}
276
+ ])
277
+ """
278
+ if not hasattr(self._adapter, "aggregate"):
279
+ raise FlashError("aggregate() is only available for MongoDB.")
280
+ return self._adapter.aggregate(table, pipeline)
281
+
282
+ def create_index(self, table: str, field: str, unique: bool = False) -> str:
283
+ """Create an index on a MongoDB collection field."""
284
+ if not hasattr(self._adapter, "create_index"):
285
+ raise FlashError("create_index() is only available for MongoDB.")
286
+ return self._adapter.create_index(table, field, unique)
287
+
288
+ # ── Utility ────────────────────────────────────────────────────────────
289
+
290
+ def ping(self) -> bool:
291
+ """Test if the database connection is alive."""
292
+ return self._adapter.ping()
293
+
294
+ def close(self):
295
+ """Close the database connection."""
296
+ self._adapter.close()
297
+
298
+ def __enter__(self):
299
+ return self
300
+
301
+ def __exit__(self, exc_type, exc_val, exc_tb):
302
+ self.close()
303
+ return False
304
+
305
+ def __repr__(self):
306
+ return f"<FlashDB type={self.db_type} db={self.config.get('database', '?')}>"
flash/exceptions.py ADDED
@@ -0,0 +1,33 @@
1
+ """
2
+ Custom exceptions for the Flash library.
3
+ """
4
+
5
+
6
+ class FlashError(Exception):
7
+ """Base exception for all Flash errors."""
8
+ pass
9
+
10
+
11
+ class ConnectionError(FlashError):
12
+ """Raised when a database connection fails."""
13
+ pass
14
+
15
+
16
+ class QueryError(FlashError):
17
+ """Raised when a query fails to execute."""
18
+ pass
19
+
20
+
21
+ class UnsupportedDatabaseError(FlashError):
22
+ """Raised when an unsupported database type is used."""
23
+ pass
24
+
25
+
26
+ class TransactionError(FlashError):
27
+ """Raised when a transaction operation fails."""
28
+ pass
29
+
30
+
31
+ class SchemaError(FlashError):
32
+ """Raised when a schema operation fails."""
33
+ pass