HandySQL 1.0.0__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.
@@ -0,0 +1,52 @@
1
+ """
2
+ HandySQL
3
+ =======
4
+ A simple and powerful SQL library for Python.
5
+
6
+ Supported databases: SQLite · MySQL · PostgreSQL
7
+
8
+ Quick start
9
+ -----------
10
+ >>> from HandySQL import HandySQL
11
+ >>>
12
+ >>> with HandySQL(db_type="sqlite", database="app.db") as db:
13
+ ... db.quick_table("users", "name", "email")
14
+ ... db.add("users", name="Alice", email="alice@example.com")
15
+ ... users = db.get_all("users")
16
+
17
+ Public API
18
+ ----------
19
+ Classes:
20
+ HandySQL – main database interface
21
+ SQLBuilder – fluent SQL query builder
22
+ DatabaseError – library exception
23
+
24
+ Functions:
25
+ create_connection(db_type, **kwargs) -> HandySQL
26
+ sql_builder(table) -> SQLBuilder
27
+ """
28
+
29
+ from .sql_easy import (
30
+ HandySQL,
31
+ SQLBuilder,
32
+ DatabaseError,
33
+ create_connection,
34
+ sql_builder,
35
+ MYSQL_AVAILABLE,
36
+ POSTGRESQL_AVAILABLE,
37
+ )
38
+
39
+ __all__ = [
40
+ "HandySQL",
41
+ "SQLBuilder",
42
+ "DatabaseError",
43
+ "create_connection",
44
+ "sql_builder",
45
+ "MYSQL_AVAILABLE",
46
+ "POSTGRESQL_AVAILABLE",
47
+ ]
48
+
49
+ __version__ = "1.0.0"
50
+ __author__ = "HandySQL Team"
51
+ __license__ = "MIT"
52
+ __url__ = "https://github.com/HandySQL/HandySQL"
@@ -0,0 +1,269 @@
1
+ """
2
+ easysql.builder
3
+ ===============
4
+ Fluent SQL query builder.
5
+
6
+ Usage
7
+ -----
8
+ >>> from easysql import sql_builder
9
+ >>>
10
+ >>> q = (sql_builder("users")
11
+ ... .select(["name", "email"])
12
+ ... .where("age > ?", 18)
13
+ ... .order_by("name")
14
+ ... .limit(10))
15
+ >>> sql, params = q.build()
16
+ """
17
+
18
+ from typing import Any, List, Tuple, Union
19
+ from .exceptions import DatabaseError
20
+
21
+
22
+ class SQLBuilder:
23
+ """
24
+ Fluent builder for SELECT / INSERT / UPDATE / DELETE statements.
25
+
26
+ Every method returns ``self``, so calls can be chained::
27
+
28
+ builder = (SQLBuilder("users")
29
+ .select("*")
30
+ .where("age > ?", 18)
31
+ .order_by("name", "ASC")
32
+ .limit(10))
33
+ sql, params = builder.build()
34
+ """
35
+
36
+ def __init__(self, table: str = None):
37
+ self.table = table
38
+ self._select = "*"
39
+ self._where = []
40
+ self._where_params = []
41
+ self._order_by = None
42
+ self._limit = None
43
+ self._offset = None
44
+ self._join = []
45
+ self._set_values = {}
46
+ self._insert_values = []
47
+ self._operation = None
48
+ self._group_by = None
49
+ self._having = None
50
+ self._distinct = False
51
+
52
+ # ------------------------------------------------------------------ #
53
+ # SELECT helpers
54
+ # ------------------------------------------------------------------ #
55
+
56
+ def select(self, columns: Union[str, List[str]] = "*") -> "SQLBuilder":
57
+ """Set the SELECT column list."""
58
+ self._operation = "SELECT"
59
+ self._select = ", ".join(columns) if isinstance(columns, list) else columns
60
+ return self
61
+
62
+ def distinct(self) -> "SQLBuilder":
63
+ """Add the DISTINCT keyword."""
64
+ self._distinct = True
65
+ return self
66
+
67
+ # ------------------------------------------------------------------ #
68
+ # WHERE / AND / OR
69
+ # ------------------------------------------------------------------ #
70
+
71
+ def where(self, condition: str, *params) -> "SQLBuilder":
72
+ """
73
+ Add a WHERE condition.
74
+ Automatically prepends AND when a condition already exists.
75
+ """
76
+ if self._where:
77
+ self._where.append(f"AND {condition}")
78
+ else:
79
+ self._where.append(condition)
80
+ self._where_params.extend(params)
81
+ return self
82
+
83
+ def and_where(self, condition: str, *params) -> "SQLBuilder":
84
+ """Explicitly add an AND condition (alias for where())."""
85
+ return self.where(condition, *params)
86
+
87
+ def or_where(self, condition: str, *params) -> "SQLBuilder":
88
+ """Add an OR condition."""
89
+ if self._where:
90
+ self._where.append(f"OR {condition}")
91
+ else:
92
+ self._where.append(condition)
93
+ self._where_params.extend(params)
94
+ return self
95
+
96
+ def where_in(self, column: str, values: List[Any]) -> "SQLBuilder":
97
+ """Add a WHERE … IN (…) condition."""
98
+ placeholders = ", ".join("?" for _ in values)
99
+ self._where.append(
100
+ f"AND {column} IN ({placeholders})" if self._where
101
+ else f"{column} IN ({placeholders})"
102
+ )
103
+ self._where_params.extend(values)
104
+ return self
105
+
106
+ def where_between(self, column: str, start: Any, end: Any) -> "SQLBuilder":
107
+ """Add a WHERE … BETWEEN … AND … condition."""
108
+ prefix = "AND " if self._where else ""
109
+ self._where.append(f"{prefix}{column} BETWEEN ? AND ?")
110
+ self._where_params.extend([start, end])
111
+ return self
112
+
113
+ def where_like(self, column: str, pattern: str) -> "SQLBuilder":
114
+ """Add a WHERE … LIKE … condition."""
115
+ prefix = "AND " if self._where else ""
116
+ self._where.append(f"{prefix}{column} LIKE ?")
117
+ self._where_params.append(pattern)
118
+ return self
119
+
120
+ def where_null(self, column: str, is_null: bool = True) -> "SQLBuilder":
121
+ """Add WHERE … IS NULL or IS NOT NULL."""
122
+ prefix = "AND " if self._where else ""
123
+ qualifier = "IS NULL" if is_null else "IS NOT NULL"
124
+ self._where.append(f"{prefix}{column} {qualifier}")
125
+ return self
126
+
127
+ # ------------------------------------------------------------------ #
128
+ # Ordering / grouping / paging
129
+ # ------------------------------------------------------------------ #
130
+
131
+ def order_by(self, column: str, direction: str = "ASC") -> "SQLBuilder":
132
+ """Set ORDER BY clause."""
133
+ self._order_by = f"{column} {direction}".strip()
134
+ return self
135
+
136
+ def group_by(self, columns: Union[str, List[str]]) -> "SQLBuilder":
137
+ """Set GROUP BY clause."""
138
+ self._group_by = ", ".join(columns) if isinstance(columns, list) else columns
139
+ return self
140
+
141
+ def having(self, condition: str) -> "SQLBuilder":
142
+ """Set HAVING clause."""
143
+ self._having = condition
144
+ return self
145
+
146
+ def limit(self, n: int) -> "SQLBuilder":
147
+ """Set LIMIT."""
148
+ self._limit = n
149
+ return self
150
+
151
+ def offset(self, n: int) -> "SQLBuilder":
152
+ """Set OFFSET."""
153
+ self._offset = n
154
+ return self
155
+
156
+ # ------------------------------------------------------------------ #
157
+ # JOINs
158
+ # ------------------------------------------------------------------ #
159
+
160
+ def join(self, table: str, on: str, join_type: str = "INNER") -> "SQLBuilder":
161
+ """Add a JOIN clause."""
162
+ self._join.append(f"{join_type} JOIN {table} ON {on}")
163
+ return self
164
+
165
+ def left_join(self, table: str, on: str) -> "SQLBuilder":
166
+ """Shorthand for LEFT JOIN."""
167
+ return self.join(table, on, "LEFT")
168
+
169
+ def right_join(self, table: str, on: str) -> "SQLBuilder":
170
+ """Shorthand for RIGHT JOIN."""
171
+ return self.join(table, on, "RIGHT")
172
+
173
+ # ------------------------------------------------------------------ #
174
+ # DML starters
175
+ # ------------------------------------------------------------------ #
176
+
177
+ def insert(self, table: str) -> "SQLBuilder":
178
+ """Start an INSERT statement."""
179
+ self._operation = "INSERT"
180
+ self.table = table
181
+ return self
182
+
183
+ def values(self, **kwargs) -> "SQLBuilder":
184
+ """Provide a row dict for INSERT."""
185
+ self._insert_values.append(kwargs)
186
+ return self
187
+
188
+ def update(self, table: str) -> "SQLBuilder":
189
+ """Start an UPDATE statement."""
190
+ self._operation = "UPDATE"
191
+ self.table = table
192
+ return self
193
+
194
+ def set(self, **kwargs) -> "SQLBuilder":
195
+ """Provide SET values for UPDATE."""
196
+ self._set_values.update(kwargs)
197
+ return self
198
+
199
+ def delete(self, table: str) -> "SQLBuilder":
200
+ """Start a DELETE statement."""
201
+ self._operation = "DELETE"
202
+ self.table = table
203
+ return self
204
+
205
+ # ------------------------------------------------------------------ #
206
+ # Build
207
+ # ------------------------------------------------------------------ #
208
+
209
+ def build(self) -> Tuple[str, List[Any]]:
210
+ """
211
+ Compile the builder into ``(sql_string, params_list)``.
212
+
213
+ Raises
214
+ ------
215
+ DatabaseError
216
+ If no operation has been set, or required data is missing.
217
+ """
218
+ if not self._operation:
219
+ raise DatabaseError("No SQL operation has been set")
220
+
221
+ sql, params = "", []
222
+
223
+ if self._operation == "SELECT":
224
+ kw = "DISTINCT " if self._distinct else ""
225
+ sql = f"SELECT {kw}{self._select} FROM {self.table}"
226
+ if self._join:
227
+ sql += " " + " ".join(self._join)
228
+ if self._where:
229
+ sql += " WHERE " + " ".join(self._where)
230
+ params.extend(self._where_params)
231
+ if self._group_by:
232
+ sql += f" GROUP BY {self._group_by}"
233
+ if self._having:
234
+ sql += f" HAVING {self._having}"
235
+ if self._order_by:
236
+ sql += f" ORDER BY {self._order_by}"
237
+ if self._limit is not None:
238
+ sql += " LIMIT ?"
239
+ params.append(self._limit)
240
+ if self._offset is not None:
241
+ sql += " OFFSET ?"
242
+ params.append(self._offset)
243
+
244
+ elif self._operation == "UPDATE":
245
+ if not self._set_values:
246
+ raise DatabaseError("No values provided for UPDATE")
247
+ set_clause = ", ".join(f"{k} = ?" for k in self._set_values)
248
+ sql = f"UPDATE {self.table} SET {set_clause}"
249
+ params.extend(self._set_values.values())
250
+ if self._where:
251
+ sql += " WHERE " + " ".join(self._where)
252
+ params.extend(self._where_params)
253
+
254
+ elif self._operation == "INSERT":
255
+ if not self._insert_values:
256
+ raise DatabaseError("No values provided for INSERT")
257
+ cols = list(self._insert_values[0].keys())
258
+ ph = ", ".join("?" for _ in cols)
259
+ rows_ph = ", ".join(f"({ph})" for _ in self._insert_values)
260
+ sql = f"INSERT INTO {self.table} ({', '.join(cols)}) VALUES {rows_ph}"
261
+ params = [v for row in self._insert_values for v in (row[c] for c in cols)]
262
+
263
+ elif self._operation == "DELETE":
264
+ sql = f"DELETE FROM {self.table}"
265
+ if self._where:
266
+ sql += " WHERE " + " ".join(self._where)
267
+ params.extend(self._where_params)
268
+
269
+ return sql, params
@@ -0,0 +1,196 @@
1
+ """
2
+ easysql.connection
3
+ ==================
4
+ Low-level connection management for SQLite, MySQL, and PostgreSQL.
5
+ """
6
+
7
+ import sqlite3
8
+ import logging
9
+ from contextlib import contextmanager
10
+ from typing import Any, List, Tuple, Union
11
+
12
+ from .exceptions import DatabaseError, ConnectionError
13
+
14
+ try:
15
+ import mysql.connector
16
+ MYSQL_AVAILABLE = True
17
+ except ImportError:
18
+ MYSQL_AVAILABLE = False
19
+
20
+ try:
21
+ import psycopg2
22
+ POSTGRESQL_AVAILABLE = True
23
+ except ImportError:
24
+ POSTGRESQL_AVAILABLE = False
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class Connection:
30
+ """
31
+ Manages a single database connection.
32
+
33
+ Parameters
34
+ ----------
35
+ db_type : str
36
+ ``"sqlite"``, ``"mysql"``, or ``"postgresql"``
37
+ **kwargs
38
+ Connection parameters (host, user, password, database, port …)
39
+ """
40
+
41
+ _ID_TYPES = {
42
+ "sqlite": "INTEGER PRIMARY KEY AUTOINCREMENT",
43
+ "mysql": "INT AUTO_INCREMENT PRIMARY KEY",
44
+ "postgresql": "SERIAL PRIMARY KEY",
45
+ }
46
+
47
+ def __init__(self, db_type: str = "sqlite", **kwargs):
48
+ self.db_type = db_type.lower()
49
+ self._conn = None
50
+ self._auto_commit = kwargs.pop("auto_commit", True)
51
+ self._params = kwargs
52
+
53
+ if self.db_type == "sqlite":
54
+ self._db_path = kwargs.get("database", ":memory:")
55
+ elif self.db_type == "mysql":
56
+ if not MYSQL_AVAILABLE:
57
+ raise ConnectionError(
58
+ "mysql-connector-python is not installed.\n"
59
+ "Run: pip install mysql-connector-python"
60
+ )
61
+ elif self.db_type == "postgresql":
62
+ if not POSTGRESQL_AVAILABLE:
63
+ raise ConnectionError(
64
+ "psycopg2 is not installed.\n"
65
+ "Run: pip install psycopg2-binary"
66
+ )
67
+ else:
68
+ raise DatabaseError(
69
+ f"Unsupported database type: '{db_type}'. "
70
+ "Choose 'sqlite', 'mysql', or 'postgresql'."
71
+ )
72
+
73
+ # ------------------------------------------------------------------ #
74
+
75
+ def open(self):
76
+ """Open (or return existing) database connection."""
77
+ if self._conn is not None:
78
+ return self._conn
79
+ try:
80
+ p = self._params
81
+ if self.db_type == "sqlite":
82
+ self._conn = sqlite3.connect(self._db_path)
83
+ self._conn.row_factory = sqlite3.Row
84
+ logger.info("SQLite connected: %s", self._db_path)
85
+ elif self.db_type == "mysql":
86
+ self._conn = mysql.connector.connect(
87
+ host=p.get("host", "localhost"),
88
+ user=p.get("user", "root"),
89
+ password=p.get("password", ""),
90
+ database=p.get("database", ""),
91
+ port=p.get("port", 3306),
92
+ )
93
+ logger.info("MySQL connected: %s", p.get("database"))
94
+ elif self.db_type == "postgresql":
95
+ self._conn = psycopg2.connect(
96
+ host=p.get("host", "localhost"),
97
+ user=p.get("user", "postgres"),
98
+ password=p.get("password", ""),
99
+ database=p.get("database", ""),
100
+ port=p.get("port", 5432),
101
+ )
102
+ logger.info("PostgreSQL connected: %s", p.get("database"))
103
+ return self._conn
104
+ except Exception as exc:
105
+ raise ConnectionError(f"Connection failed: {exc}") from exc
106
+
107
+ def close(self):
108
+ """Close the connection if open."""
109
+ if self._conn:
110
+ self._conn.close()
111
+ self._conn = None
112
+ logger.info("Connection closed")
113
+
114
+ def __enter__(self):
115
+ self.open()
116
+ return self
117
+
118
+ def __exit__(self, *_):
119
+ self.close()
120
+
121
+ # ------------------------------------------------------------------ #
122
+
123
+ @contextmanager
124
+ def cursor(self):
125
+ """Context manager that yields a cursor and handles commit/rollback."""
126
+ if self._conn is None:
127
+ self.open()
128
+ cur = self._conn.cursor()
129
+ try:
130
+ yield cur
131
+ if self._auto_commit:
132
+ self._conn.commit()
133
+ except Exception as exc:
134
+ self._conn.rollback()
135
+ logger.error("Rolled back: %s", exc)
136
+ raise DatabaseError(f"Database operation error: {exc}") from exc
137
+ finally:
138
+ cur.close()
139
+
140
+ # ------------------------------------------------------------------ #
141
+
142
+ def execute(self, sql: str, params: Union[Tuple, List] = None) -> Any:
143
+ """
144
+ Run a SQL statement.
145
+
146
+ Returns
147
+ -------
148
+ list[dict] for SELECT
149
+ int (rowcount) for INSERT / UPDATE / DELETE
150
+ """
151
+ with self.cursor() as cur:
152
+ logger.debug("SQL: %s | params: %s", sql, params)
153
+ cur.execute(sql, params or ())
154
+ if sql.strip().upper().startswith("SELECT"):
155
+ rows = cur.fetchall()
156
+ if self.db_type == "sqlite":
157
+ return [dict(r) for r in rows]
158
+ cols = [c[0] for c in cur.description]
159
+ return [dict(zip(cols, r)) for r in rows]
160
+ return cur.rowcount
161
+
162
+ def execute_many(self, sql: str, params_list: List[Tuple]) -> int:
163
+ """Run a parameterised statement for each item in params_list."""
164
+ with self.cursor() as cur:
165
+ cur.executemany(sql, params_list)
166
+ return cur.rowcount
167
+
168
+ # ------------------------------------------------------------------ #
169
+ # Transaction helpers
170
+ # ------------------------------------------------------------------ #
171
+
172
+ def begin(self):
173
+ """Disable auto-commit to start a manual transaction."""
174
+ if self._conn is None:
175
+ self.open()
176
+ self._auto_commit = False
177
+ logger.info("Transaction started")
178
+
179
+ def commit(self):
180
+ """Commit the current transaction."""
181
+ if self._conn:
182
+ self._conn.commit()
183
+ logger.info("Transaction committed")
184
+
185
+ def rollback(self):
186
+ """Roll back the current transaction."""
187
+ if self._conn:
188
+ self._conn.rollback()
189
+ logger.info("Transaction rolled back")
190
+
191
+ # ------------------------------------------------------------------ #
192
+
193
+ @property
194
+ def auto_id_sql(self) -> str:
195
+ """Return the correct auto-increment primary key definition."""
196
+ return self._ID_TYPES[self.db_type]
@@ -0,0 +1,40 @@
1
+ """
2
+ easysql.exceptions
3
+ ==================
4
+ All exceptions raised by the EasySQL library.
5
+ """
6
+
7
+
8
+ class DatabaseError(Exception):
9
+ """
10
+ Raised for any EasySQL / database-level error.
11
+
12
+ Examples
13
+ --------
14
+ >>> from easysql import DatabaseError
15
+ >>> try:
16
+ ... db.remove("users") # missing filter
17
+ ... except DatabaseError as e:
18
+ ... print(f"Caught: {e}")
19
+ """
20
+ pass
21
+
22
+
23
+ class ConnectionError(DatabaseError):
24
+ """Raised when a connection to the database cannot be established."""
25
+ pass
26
+
27
+
28
+ class TableNotFoundError(DatabaseError):
29
+ """Raised when an operation targets a table that does not exist."""
30
+ pass
31
+
32
+
33
+ class RecordNotFoundError(DatabaseError):
34
+ """Raised when a required row is not found (e.g. clone_row with bad ID)."""
35
+ pass
36
+
37
+
38
+ class ValidationError(DatabaseError):
39
+ """Raised when input data fails a safety check (e.g. empty filter on delete)."""
40
+ pass