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.
flash/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ Flash - A unified database interface for MySQL, PostgreSQL, and MongoDB.
3
+ """
4
+
5
+ from .core import FlashDB
6
+ from .exceptions import FlashError, ConnectionError, QueryError
7
+
8
+ __version__ = "1.0.0"
9
+ __author__ = "Flash Team"
10
+ __all__ = ["FlashDB", "FlashError", "ConnectionError", "QueryError"]
File without changes
@@ -0,0 +1,247 @@
1
+ """
2
+ MongoDB adapter for Flash.
3
+ Requires: pip install pymongo
4
+ """
5
+
6
+ from typing import Any, Dict, List, Optional
7
+ from ..filters import filters_to_mongo
8
+ from ..exceptions import ConnectionError, QueryError
9
+
10
+
11
+ class MongoAdapter:
12
+ """
13
+ MongoDB backend adapter for FlashDB.
14
+ Uses pymongo under the hood.
15
+ """
16
+
17
+ def __init__(self, config: dict):
18
+ self.config = config
19
+ self.client = None
20
+ self.db = None
21
+ self._connect()
22
+
23
+ def _connect(self):
24
+ try:
25
+ from pymongo import MongoClient
26
+ from pymongo.errors import ConnectionFailure
27
+
28
+ uri = self.config.get("uri")
29
+ if uri:
30
+ self.client = MongoClient(uri)
31
+ else:
32
+ host = self.config.get("host", "localhost")
33
+ port = self.config.get("port", 27017)
34
+ user = self.config.get("user")
35
+ password = self.config.get("password")
36
+
37
+ if user and password:
38
+ self.client = MongoClient(
39
+ host=host, port=port, username=user, password=password
40
+ )
41
+ else:
42
+ self.client = MongoClient(host=host, port=port)
43
+
44
+ db_name = self.config.get("database", "flash_db")
45
+ self.db = self.client[db_name]
46
+
47
+ # Test connection
48
+ self.client.admin.command("ping")
49
+
50
+ except ImportError:
51
+ raise ConnectionError(
52
+ "pymongo is not installed. Run: pip install pymongo"
53
+ )
54
+ except Exception as e:
55
+ raise ConnectionError(f"MongoDB connection failed: {e}")
56
+
57
+ def _col(self, table: str):
58
+ """Return the pymongo Collection for the given table/collection name."""
59
+ return self.db[table]
60
+
61
+ def _clean(self, doc) -> dict:
62
+ """Convert ObjectId to string for clean output."""
63
+ if doc and "_id" in doc:
64
+ doc["_id"] = str(doc["_id"])
65
+ return doc
66
+
67
+ # ── Core CRUD ──────────────────────────────────────────────────────────
68
+
69
+ def all(self, table: str) -> List[dict]:
70
+ return [self._clean(d) for d in self._col(table).find()]
71
+
72
+ def select(self, table: str, fields: List[str] = None, filters: dict = None,
73
+ limit: int = None, offset: int = None, order_by: str = None) -> List[dict]:
74
+ query = filters_to_mongo(filters) if filters else {}
75
+ projection = {f: 1 for f in fields} if fields else None
76
+
77
+ cursor = self._col(table).find(query, projection)
78
+
79
+ if order_by:
80
+ # Support "field" (asc) or "-field" (desc)
81
+ if order_by.startswith("-"):
82
+ cursor = cursor.sort(order_by[1:], -1)
83
+ else:
84
+ cursor = cursor.sort(order_by, 1)
85
+
86
+ if offset:
87
+ cursor = cursor.skip(int(offset))
88
+ if limit:
89
+ cursor = cursor.limit(int(limit))
90
+
91
+ return [self._clean(d) for d in cursor]
92
+
93
+ def add(self, table: str, data: dict) -> str:
94
+ try:
95
+ result = self._col(table).insert_one(data.copy())
96
+ return str(result.inserted_id)
97
+ except Exception as e:
98
+ raise QueryError(f"MongoDB insert failed: {e}")
99
+
100
+ def bulk_insert(self, table: str, records: List[dict]) -> int:
101
+ if not records:
102
+ return 0
103
+ try:
104
+ result = self._col(table).insert_many([r.copy() for r in records])
105
+ return len(result.inserted_ids)
106
+ except Exception as e:
107
+ raise QueryError(f"MongoDB bulk insert failed: {e}")
108
+
109
+ def update(self, table: str, filters: dict, data: dict) -> int:
110
+ query = filters_to_mongo(filters)
111
+ try:
112
+ result = self._col(table).update_many(query, {"$set": data})
113
+ return result.modified_count
114
+ except Exception as e:
115
+ raise QueryError(f"MongoDB update failed: {e}")
116
+
117
+ def delete(self, table: str, filters: dict = None) -> int:
118
+ query = filters_to_mongo(filters) if filters else {}
119
+ try:
120
+ result = self._col(table).delete_many(query)
121
+ return result.deleted_count
122
+ except Exception as e:
123
+ raise QueryError(f"MongoDB delete failed: {e}")
124
+
125
+ # ── Schema / Collection Operations ─────────────────────────────────────
126
+
127
+ def create_table(self, table: str, schema: dict = None, primary_key: str = "id") -> bool:
128
+ """
129
+ In MongoDB, collections are created implicitly.
130
+ This optionally applies a JSON Schema validator.
131
+ """
132
+ if schema:
133
+ props = {}
134
+ for field, typ in schema.items():
135
+ bson_type = self._py_to_bson(typ)
136
+ props[field] = {"bsonType": bson_type}
137
+
138
+ validator = {
139
+ "$jsonSchema": {
140
+ "bsonType": "object",
141
+ "properties": props,
142
+ }
143
+ }
144
+ try:
145
+ self.db.create_collection(table, validator=validator)
146
+ except Exception:
147
+ # Collection may already exist; apply collMod instead
148
+ self.db.command("collMod", table, validator=validator)
149
+ else:
150
+ if table not in self.db.list_collection_names():
151
+ self.db.create_collection(table)
152
+ return True
153
+
154
+ def drop_table(self, table: str) -> bool:
155
+ self._col(table).drop()
156
+ return True
157
+
158
+ def truncate(self, table: str) -> bool:
159
+ self._col(table).delete_many({})
160
+ return True
161
+
162
+ def show_tables(self) -> List[str]:
163
+ return self.db.list_collection_names()
164
+
165
+ def describe(self, table: str) -> dict:
166
+ """Return collection stats as schema info."""
167
+ return self.db.command("collStats", table)
168
+
169
+ def create_index(self, table: str, field: str, unique: bool = False) -> str:
170
+ from pymongo import ASCENDING
171
+ result = self._col(table).create_index([(field, ASCENDING)], unique=unique)
172
+ return result
173
+
174
+ # ── Transactions (MongoDB 4.0+ with replica set) ───────────────────────
175
+
176
+ def begin(self):
177
+ self._session = self.client.start_session()
178
+ self._session.start_transaction()
179
+
180
+ def commit(self):
181
+ if hasattr(self, "_session"):
182
+ self._session.commit_transaction()
183
+ self._session.end_session()
184
+
185
+ def rollback(self):
186
+ if hasattr(self, "_session"):
187
+ self._session.abort_transaction()
188
+ self._session.end_session()
189
+
190
+ # ── Raw Query ──────────────────────────────────────────────────────────
191
+
192
+ def raw(self, command: dict, table: str = None) -> Any:
193
+ """
194
+ Execute a raw MongoDB command.
195
+ For collection-level ops pass table; otherwise it runs on the DB.
196
+ """
197
+ try:
198
+ if table:
199
+ return list(self._col(table).find(command))
200
+ return self.db.command(command)
201
+ except Exception as e:
202
+ raise QueryError(f"MongoDB raw command failed: {e}")
203
+
204
+ # ── Count ──────────────────────────────────────────────────────────────
205
+
206
+ def count(self, table: str, filters: dict = None) -> int:
207
+ query = filters_to_mongo(filters) if filters else {}
208
+ return self._col(table).count_documents(query)
209
+
210
+ # ── Aggregation ────────────────────────────────────────────────────────
211
+
212
+ def aggregate(self, table: str, pipeline: list) -> List[dict]:
213
+ return [self._clean(d) for d in self._col(table).aggregate(pipeline)]
214
+
215
+ # ── Lifecycle ──────────────────────────────────────────────────────────
216
+
217
+ def close(self):
218
+ if self.client:
219
+ self.client.close()
220
+
221
+ def ping(self) -> bool:
222
+ try:
223
+ self.client.admin.command("ping")
224
+ return True
225
+ except Exception:
226
+ return False
227
+
228
+ # ── Helpers ────────────────────────────────────────────────────────────
229
+
230
+ def _py_to_bson(self, type_str: str) -> str:
231
+ mapping = {
232
+ "int": "int",
233
+ "integer": "int",
234
+ "str": "string",
235
+ "string": "string",
236
+ "text": "string",
237
+ "float": "double",
238
+ "double": "double",
239
+ "bool": "bool",
240
+ "boolean": "bool",
241
+ "date": "date",
242
+ "datetime": "date",
243
+ "list": "array",
244
+ "dict": "object",
245
+ "json": "object",
246
+ }
247
+ return mapping.get(type_str.lower(), "string")
@@ -0,0 +1,217 @@
1
+ """
2
+ MySQL adapter for Flash.
3
+ Requires: pip install mysql-connector-python
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 MySQLAdapter:
12
+ """
13
+ MySQL backend adapter for FlashDB.
14
+ Uses mysql-connector-python 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 mysql.connector
27
+ self.conn = mysql.connector.connect(
28
+ host=self.config.get("host", "localhost"),
29
+ port=self.config.get("port", 3306),
30
+ user=self.config.get("user", "root"),
31
+ password=self.config.get("password", ""),
32
+ database=self.config.get("database"),
33
+ autocommit=True,
34
+ )
35
+ self.cursor = self.conn.cursor(dictionary=True)
36
+ except ImportError:
37
+ raise ConnectionError(
38
+ "mysql-connector-python is not installed. Run: pip install mysql-connector-python"
39
+ )
40
+ except Exception as e:
41
+ raise ConnectionError(f"MySQL connection failed: {e}")
42
+
43
+ def _execute(self, sql: str, params: list = None) -> Any:
44
+ try:
45
+ self.cursor.execute(sql, params or [])
46
+ return self.cursor
47
+ except Exception as e:
48
+ raise QueryError(f"MySQL query failed: {e}\nSQL: {sql}\nParams: {params}")
49
+
50
+ # ── Core CRUD ──────────────────────────────────────────────────────────
51
+
52
+ def all(self, table: str) -> List[dict]:
53
+ self._execute(f"SELECT * FROM `{table}`")
54
+ return self.cursor.fetchall()
55
+
56
+ def select(self, table: str, fields: List[str] = None, filters: dict = None,
57
+ limit: int = None, offset: int = None, order_by: str = None) -> List[dict]:
58
+ cols = ", ".join(f"`{f}`" for f in fields) if fields else "*"
59
+ sql = f"SELECT {cols} FROM `{table}`"
60
+ params = []
61
+
62
+ if filters:
63
+ clause, params = filters_to_sql(filters)
64
+ sql += f" WHERE {clause}"
65
+
66
+ if order_by:
67
+ sql += f" ORDER BY {order_by}"
68
+
69
+ if limit is not None:
70
+ sql += f" LIMIT {int(limit)}"
71
+ if offset is not None:
72
+ sql += f" OFFSET {int(offset)}"
73
+
74
+ self._execute(sql, params)
75
+ return self.cursor.fetchall()
76
+
77
+ def add(self, table: str, data: dict) -> int:
78
+ fields = ", ".join(f"`{k}`" for k in data.keys())
79
+ placeholders = ", ".join(["%s"] * len(data))
80
+ sql = f"INSERT INTO `{table}` ({fields}) VALUES ({placeholders})"
81
+ self._execute(sql, list(data.values()))
82
+ if not self._in_transaction:
83
+ self.conn.commit()
84
+ return self.cursor.lastrowid
85
+
86
+ def bulk_insert(self, table: str, records: List[dict]) -> int:
87
+ if not records:
88
+ return 0
89
+ fields = ", ".join(f"`{k}`" for k in records[0].keys())
90
+ placeholders = ", ".join(["%s"] * len(records[0]))
91
+ sql = f"INSERT INTO `{table}` ({fields}) VALUES ({placeholders})"
92
+ rows = [list(r.values()) for r in records]
93
+ try:
94
+ self.cursor.executemany(sql, rows)
95
+ if not self._in_transaction:
96
+ self.conn.commit()
97
+ return self.cursor.rowcount
98
+ except Exception as e:
99
+ raise QueryError(f"MySQL bulk insert failed: {e}")
100
+
101
+ def update(self, table: str, filters: dict, data: dict) -> int:
102
+ set_clause, set_params = build_update_sql(data)
103
+ where_clause, where_params = filters_to_sql(filters)
104
+ sql = f"UPDATE `{table}` SET {set_clause}"
105
+ if where_clause:
106
+ sql += f" WHERE {where_clause}"
107
+ self._execute(sql, set_params + where_params)
108
+ if not self._in_transaction:
109
+ self.conn.commit()
110
+ return self.cursor.rowcount
111
+
112
+ def delete(self, table: str, filters: dict = None) -> int:
113
+ sql = f"DELETE FROM `{table}`"
114
+ params = []
115
+ if filters:
116
+ clause, params = filters_to_sql(filters)
117
+ sql += f" WHERE {clause}"
118
+ self._execute(sql, params)
119
+ if not self._in_transaction:
120
+ self.conn.commit()
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, "mysql")
130
+ if col == primary_key:
131
+ col_defs.append(f"`{col}` {sql_type} AUTO_INCREMENT PRIMARY KEY")
132
+ has_pk = True
133
+ else:
134
+ col_defs.append(f"`{col}` {sql_type}")
135
+ if not has_pk:
136
+ col_defs.insert(0, f"`{primary_key}` INT AUTO_INCREMENT PRIMARY KEY")
137
+
138
+ sql = f"CREATE TABLE IF NOT EXISTS `{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 `{table}`")
144
+ return True
145
+
146
+ def truncate(self, table: str) -> bool:
147
+ self._execute(f"TRUNCATE TABLE `{table}`")
148
+ return True
149
+
150
+ def show_tables(self) -> List[str]:
151
+ self._execute("SHOW TABLES")
152
+ rows = self.cursor.fetchall()
153
+ return [list(r.values())[0] for r in rows]
154
+
155
+ def describe(self, table: str) -> List[dict]:
156
+ self._execute(f"DESCRIBE `{table}`")
157
+ return self.cursor.fetchall()
158
+
159
+ # ── Transactions ───────────────────────────────────────────────────────
160
+
161
+ def begin(self):
162
+ self.conn.autocommit = False
163
+ self._in_transaction = True
164
+
165
+ def commit(self):
166
+ try:
167
+ self.conn.commit()
168
+ except Exception as e:
169
+ raise TransactionError(f"Commit failed: {e}")
170
+ finally:
171
+ self.conn.autocommit = True
172
+ self._in_transaction = False
173
+
174
+ def rollback(self):
175
+ try:
176
+ self.conn.rollback()
177
+ except Exception as e:
178
+ raise TransactionError(f"Rollback failed: {e}")
179
+ finally:
180
+ self.conn.autocommit = True
181
+ self._in_transaction = False
182
+
183
+ # ── Raw Query ──────────────────────────────────────────────────────────
184
+
185
+ def raw(self, sql: str, params: list = None) -> Any:
186
+ self._execute(sql, params)
187
+ try:
188
+ return self.cursor.fetchall()
189
+ except Exception:
190
+ return self.cursor.rowcount
191
+
192
+ # ── Count ──────────────────────────────────────────────────────────────
193
+
194
+ def count(self, table: str, filters: dict = None) -> int:
195
+ sql = f"SELECT COUNT(*) as cnt FROM `{table}`"
196
+ params = []
197
+ if filters:
198
+ clause, params = filters_to_sql(filters)
199
+ sql += f" WHERE {clause}"
200
+ self._execute(sql, params)
201
+ row = self.cursor.fetchone()
202
+ return row["cnt"] if row else 0
203
+
204
+ # ── Lifecycle ──────────────────────────────────────────────────────────
205
+
206
+ def close(self):
207
+ if self.cursor:
208
+ self.cursor.close()
209
+ if self.conn:
210
+ self.conn.close()
211
+
212
+ def ping(self) -> bool:
213
+ try:
214
+ self.conn.ping(reconnect=True)
215
+ return True
216
+ except Exception:
217
+ return False