future-framework 1.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.
- future/__init__.py +3 -0
- future/application.py +656 -0
- future/authentication/Auth0Authentication.py +9 -0
- future/authentication/Authentication.py +11 -0
- future/authentication/AzureADAuthentication.py +9 -0
- future/authentication/BasicAuthentication.py +9 -0
- future/authentication/KerberosAuthentication.py +9 -0
- future/authentication/KeycloakAuthentication.py +9 -0
- future/authentication/OAuth2Authentication.py +9 -0
- future/authentication/OpenIdConnectAuthentication.py +9 -0
- future/authentication/SAMLAuthentication.py +9 -0
- future/cli/__init__.py +1 -0
- future/cli/main.py +608 -0
- future/cli/stubs.py +114 -0
- future/controllers/__init__.py +4 -0
- future/controllers/base.py +8 -0
- future/controllers/builtins.py +41 -0
- future/controllers/graphql.py +23 -0
- future/controllers/openapi.py +224 -0
- future/databases/Clickhouse.py +176 -0
- future/databases/Connections.py +20 -0
- future/databases/Database.py +66 -0
- future/databases/Elasticsearch.py +146 -0
- future/databases/MongoDB.py +177 -0
- future/databases/MySQL.py +215 -0
- future/databases/Postgres.py +214 -0
- future/databases/Redis.py +146 -0
- future/databases/SQLite.py +219 -0
- future/exceptions.py +27 -0
- future/graphql/__init__.py +1 -0
- future/graphql/schema.py +148 -0
- future/lifespan.py +70 -0
- future/logger.py +53 -0
- future/middleware/Middleware.py +201 -0
- future/middleware/SessionMiddleware.py +77 -0
- future/middleware/__init__.py +21 -0
- future/migrations/Blueprint.py +54 -0
- future/migrations/Column.py +33 -0
- future/migrations/Migration.py +11 -0
- future/migrations/MigrationGenerator.py +137 -0
- future/migrations/Migrator.py +72 -0
- future/migrations/Schema.py +21 -0
- future/models/__init__.py +1 -0
- future/models/model.py +224 -0
- future/openapi.py +233 -0
- future/plugins/ElasticsearchPlugin.py +496 -0
- future/plugins/__init__.py +9 -0
- future/request.py +143 -0
- future/response.py +201 -0
- future/routing.py +342 -0
- future/seeds/SeedGenerator.py +95 -0
- future/seeds/SeedRunner.py +42 -0
- future/seeds/Seeder.py +9 -0
- future/settings.py +94 -0
- future/tasks/__init__.py +14 -0
- future/tasks/scheduler.py +267 -0
- future/testing/__init__.py +1 -0
- future/testing/client.py +135 -0
- future/types.py +47 -0
- future_framework-1.1.0.dist-info/METADATA +68 -0
- future_framework-1.1.0.dist-info/RECORD +64 -0
- future_framework-1.1.0.dist-info/WHEEL +4 -0
- future_framework-1.1.0.dist-info/entry_points.txt +3 -0
- future_framework-1.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from future.databases.Database import Database
|
|
2
|
+
from future.plugins.ElasticsearchPlugin import ElasticsearchPlugin
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Elasticsearch(Database):
|
|
6
|
+
def __init__(self, host: str, port: int, username: str, password: str, database: str = ""):
|
|
7
|
+
super().__init__(host, port, username, password, database)
|
|
8
|
+
self.client = ElasticsearchPlugin(host=host, username=username, password=password)
|
|
9
|
+
|
|
10
|
+
def connect(self):
|
|
11
|
+
return self.client
|
|
12
|
+
|
|
13
|
+
def disconnect(self):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def save(self, model):
|
|
17
|
+
name = model.tableize()
|
|
18
|
+
return self.client.index_document(name, model.to_dict(), document_id=getattr(model, "id", None))
|
|
19
|
+
|
|
20
|
+
def find(self, model, id):
|
|
21
|
+
result = self.client.get_document(model.tableize(), id)
|
|
22
|
+
if not result or "error" in result or "_source" not in result:
|
|
23
|
+
return None
|
|
24
|
+
return model.__class__(**result["_source"])
|
|
25
|
+
|
|
26
|
+
def all(self, model):
|
|
27
|
+
result = self.client.search_documents(model.tableize(), {"query": {"match_all": {}}, "size": 10000})
|
|
28
|
+
if not result or "error" in result:
|
|
29
|
+
return []
|
|
30
|
+
return [model.__class__(**hit["_source"]) for hit in result.get("hits", {}).get("hits", [])]
|
|
31
|
+
|
|
32
|
+
def get(self, model, wheres, limit=None, orders=None):
|
|
33
|
+
must = []
|
|
34
|
+
for column, operator, value in wheres:
|
|
35
|
+
if operator == "=":
|
|
36
|
+
must.append({"match": {column: value}})
|
|
37
|
+
elif operator == ">":
|
|
38
|
+
must.append({"range": {column: {"gt": value}}})
|
|
39
|
+
elif operator == ">=":
|
|
40
|
+
must.append({"range": {column: {"gte": value}}})
|
|
41
|
+
elif operator == "<":
|
|
42
|
+
must.append({"range": {column: {"lt": value}}})
|
|
43
|
+
elif operator == "<=":
|
|
44
|
+
must.append({"range": {column: {"lte": value}}})
|
|
45
|
+
elif operator == "!=":
|
|
46
|
+
must.append({"bool": {"must_not": [{"match": {column: value}}]}})
|
|
47
|
+
elif operator == "in":
|
|
48
|
+
must.append({"terms": {column: list(value)}})
|
|
49
|
+
elif operator == "like":
|
|
50
|
+
pattern = str(value).replace("%", "*").replace("_", "?")
|
|
51
|
+
must.append({"wildcard": {column: pattern}})
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(f"Unsupported operator: {operator}")
|
|
54
|
+
size = limit if limit is not None else 10000
|
|
55
|
+
body = {"query": {"bool": {"must": must}}, "size": size}
|
|
56
|
+
if orders:
|
|
57
|
+
body["sort"] = [{column: {"order": direction}} for column, direction in orders]
|
|
58
|
+
result = self.client.search_documents(model.tableize(), body)
|
|
59
|
+
if not result or "error" in result:
|
|
60
|
+
return []
|
|
61
|
+
return [model.__class__(**hit["_source"]) for hit in result.get("hits", {}).get("hits", [])]
|
|
62
|
+
|
|
63
|
+
def delete(self, model):
|
|
64
|
+
return self.client.delete_document(model.tableize(), getattr(model, "id", None))
|
|
65
|
+
|
|
66
|
+
def update(self, model, changes):
|
|
67
|
+
return self.client.update_document(model.tableize(), getattr(model, "id", None), changes)
|
|
68
|
+
|
|
69
|
+
def schema_create(self, blueprint):
|
|
70
|
+
properties = {}
|
|
71
|
+
for column in blueprint.columns:
|
|
72
|
+
if column.type == "string" and (column.is_primary or column.name == "id"):
|
|
73
|
+
properties[column.name] = {"type": "keyword"}
|
|
74
|
+
elif column.type == "string":
|
|
75
|
+
properties[column.name] = {"type": "text", "fields": {"keyword": {"type": "keyword", "ignore_above": 256}}}
|
|
76
|
+
elif column.type == "text":
|
|
77
|
+
properties[column.name] = {"type": "text"}
|
|
78
|
+
elif column.type == "integer":
|
|
79
|
+
properties[column.name] = {"type": "long"}
|
|
80
|
+
elif column.type == "float":
|
|
81
|
+
properties[column.name] = {"type": "double"}
|
|
82
|
+
elif column.type == "boolean":
|
|
83
|
+
properties[column.name] = {"type": "boolean"}
|
|
84
|
+
elif column.type == "datetime":
|
|
85
|
+
properties[column.name] = {"type": "date"}
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError(f"Unsupported column type: {column.type}")
|
|
88
|
+
mapping = {"properties": properties}
|
|
89
|
+
if self.client.index_exists(blueprint.name):
|
|
90
|
+
return {"status": "exists", "index": blueprint.name}
|
|
91
|
+
result = self.client.create_index(blueprint.name, mapping=mapping)
|
|
92
|
+
if isinstance(result, dict) and "error" in result:
|
|
93
|
+
raise RuntimeError(result["error"])
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
def schema_drop(self, name):
|
|
97
|
+
if not self.client.index_exists(name):
|
|
98
|
+
return {"status": "missing", "index": name}
|
|
99
|
+
result = self.client.delete_index(name)
|
|
100
|
+
if isinstance(result, dict) and "error" in result:
|
|
101
|
+
raise RuntimeError(result["error"])
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
def _ensure_migrations_index(self):
|
|
105
|
+
if self.client.index_exists("migrations"):
|
|
106
|
+
return
|
|
107
|
+
mapping = {"properties": {"name": {"type": "keyword"}, "batch": {"type": "integer"}}}
|
|
108
|
+
result = self.client.create_index("migrations", mapping=mapping)
|
|
109
|
+
if isinstance(result, dict) and "error" in result:
|
|
110
|
+
raise RuntimeError(result["error"])
|
|
111
|
+
|
|
112
|
+
def migrations_get(self):
|
|
113
|
+
self._ensure_migrations_index()
|
|
114
|
+
result = self.client.search_documents("migrations", {"query": {"match_all": {}}, "size": 10000, "sort": [{"batch": "asc"}, {"name": "asc"}]})
|
|
115
|
+
if not result or "error" in result:
|
|
116
|
+
return []
|
|
117
|
+
return [hit["_source"]["name"] for hit in result.get("hits", {}).get("hits", [])]
|
|
118
|
+
|
|
119
|
+
def migrations_put(self, name, batch):
|
|
120
|
+
self._ensure_migrations_index()
|
|
121
|
+
result = self.client.index_document("migrations", {"name": name, "batch": batch}, document_id=name)
|
|
122
|
+
if isinstance(result, dict) and "error" in result:
|
|
123
|
+
raise RuntimeError(result["error"])
|
|
124
|
+
|
|
125
|
+
def migrations_delete(self, name):
|
|
126
|
+
self._ensure_migrations_index()
|
|
127
|
+
result = self.client.delete_document("migrations", name)
|
|
128
|
+
if isinstance(result, dict) and "error" in result:
|
|
129
|
+
raise RuntimeError(result["error"])
|
|
130
|
+
|
|
131
|
+
def migrations_max_batch(self):
|
|
132
|
+
self._ensure_migrations_index()
|
|
133
|
+
result = self.client.search_documents("migrations", {"query": {"match_all": {}}, "size": 0, "aggs": {"max_batch": {"max": {"field": "batch"}}}})
|
|
134
|
+
if not result or "error" in result:
|
|
135
|
+
return 0
|
|
136
|
+
value = result.get("aggregations", {}).get("max_batch", {}).get("value")
|
|
137
|
+
if value is None:
|
|
138
|
+
return 0
|
|
139
|
+
return int(value)
|
|
140
|
+
|
|
141
|
+
def migrations_batch_for(self, name):
|
|
142
|
+
self._ensure_migrations_index()
|
|
143
|
+
result = self.client.get_document("migrations", name)
|
|
144
|
+
if not result or "error" in result or "_source" not in result:
|
|
145
|
+
return None
|
|
146
|
+
return int(result["_source"]["batch"])
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from future.databases.Database import Database
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MongoDB(Database):
|
|
6
|
+
def __init__(self, host: str, port: int, username: str, password: str, database: str):
|
|
7
|
+
super().__init__(host, port, username, password, database)
|
|
8
|
+
self.client = None
|
|
9
|
+
self.db = None
|
|
10
|
+
|
|
11
|
+
def connect(self):
|
|
12
|
+
from pymongo import MongoClient
|
|
13
|
+
if self.username:
|
|
14
|
+
uri = f"mongodb://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}"
|
|
15
|
+
else:
|
|
16
|
+
uri = f"mongodb://{self.host}:{self.port}/{self.database}"
|
|
17
|
+
self.client = MongoClient(uri)
|
|
18
|
+
self.db = self.client[self.database]
|
|
19
|
+
return self.client
|
|
20
|
+
|
|
21
|
+
def disconnect(self):
|
|
22
|
+
if self.client is not None:
|
|
23
|
+
self.client.close()
|
|
24
|
+
self.client = None
|
|
25
|
+
self.db = None
|
|
26
|
+
|
|
27
|
+
def save(self, model):
|
|
28
|
+
if self.db is None:
|
|
29
|
+
self.connect()
|
|
30
|
+
name = model.tableize()
|
|
31
|
+
data = model.to_dict()
|
|
32
|
+
doc_id = data.get("id")
|
|
33
|
+
if doc_id is not None:
|
|
34
|
+
self.db[name].replace_one({"id": doc_id}, data, upsert=True)
|
|
35
|
+
else:
|
|
36
|
+
self.db[name].insert_one(data)
|
|
37
|
+
return {"status": "saved", "collection": name, "id": doc_id}
|
|
38
|
+
|
|
39
|
+
def find(self, model, id):
|
|
40
|
+
if self.db is None:
|
|
41
|
+
self.connect()
|
|
42
|
+
doc = self.db[model.tableize()].find_one({"id": id}, {"_id": 0})
|
|
43
|
+
if doc is None:
|
|
44
|
+
return None
|
|
45
|
+
return model.__class__(**doc)
|
|
46
|
+
|
|
47
|
+
def all(self, model):
|
|
48
|
+
if self.db is None:
|
|
49
|
+
self.connect()
|
|
50
|
+
return [model.__class__(**doc) for doc in self.db[model.tableize()].find({}, {"_id": 0})]
|
|
51
|
+
|
|
52
|
+
def get(self, model, wheres, limit=None, orders=None):
|
|
53
|
+
if self.db is None:
|
|
54
|
+
self.connect()
|
|
55
|
+
query = {}
|
|
56
|
+
for column, operator, value in wheres:
|
|
57
|
+
if operator == "=":
|
|
58
|
+
query[column] = value
|
|
59
|
+
elif operator == "in":
|
|
60
|
+
query[column] = {"$in": list(value)}
|
|
61
|
+
elif operator == ">":
|
|
62
|
+
query[column] = {"$gt": value}
|
|
63
|
+
elif operator == ">=":
|
|
64
|
+
query[column] = {"$gte": value}
|
|
65
|
+
elif operator == "<":
|
|
66
|
+
query[column] = {"$lt": value}
|
|
67
|
+
elif operator == "<=":
|
|
68
|
+
query[column] = {"$lte": value}
|
|
69
|
+
elif operator == "!=":
|
|
70
|
+
query[column] = {"$ne": value}
|
|
71
|
+
elif operator == "like":
|
|
72
|
+
parts = []
|
|
73
|
+
for char in str(value):
|
|
74
|
+
if char == "%":
|
|
75
|
+
parts.append(".*")
|
|
76
|
+
elif char == "_":
|
|
77
|
+
parts.append(".")
|
|
78
|
+
else:
|
|
79
|
+
parts.append(re.escape(char))
|
|
80
|
+
query[column] = {"$regex": "^" + "".join(parts) + "$"}
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError(f"Unsupported operator: {operator}")
|
|
83
|
+
cursor = self.db[model.tableize()].find(query, {"_id": 0})
|
|
84
|
+
if orders:
|
|
85
|
+
cursor = cursor.sort([(column, 1 if str(direction).upper() == "ASC" else -1) for column, direction in orders])
|
|
86
|
+
if limit is not None:
|
|
87
|
+
cursor = cursor.limit(int(limit))
|
|
88
|
+
return [model.__class__(**doc) for doc in cursor]
|
|
89
|
+
|
|
90
|
+
def delete(self, model):
|
|
91
|
+
if self.db is None:
|
|
92
|
+
self.connect()
|
|
93
|
+
name = model.tableize()
|
|
94
|
+
doc_id = getattr(model, "id", None)
|
|
95
|
+
self.db[name].delete_one({"id": doc_id})
|
|
96
|
+
return {"status": "deleted", "collection": name, "id": doc_id}
|
|
97
|
+
|
|
98
|
+
def update(self, model, changes):
|
|
99
|
+
if self.db is None:
|
|
100
|
+
self.connect()
|
|
101
|
+
name = model.tableize()
|
|
102
|
+
doc_id = getattr(model, "id", None)
|
|
103
|
+
if not changes:
|
|
104
|
+
return {"status": "noop", "collection": name, "id": doc_id}
|
|
105
|
+
self.db[name].update_one({"id": doc_id}, {"$set": changes})
|
|
106
|
+
return {"status": "updated", "collection": name, "id": doc_id}
|
|
107
|
+
|
|
108
|
+
def schema_create(self, blueprint):
|
|
109
|
+
if self.db is None:
|
|
110
|
+
self.connect()
|
|
111
|
+
properties = {}
|
|
112
|
+
required = []
|
|
113
|
+
for column in blueprint.columns:
|
|
114
|
+
if column.type == "string" or column.type == "text":
|
|
115
|
+
bson_type = "string"
|
|
116
|
+
elif column.type == "integer":
|
|
117
|
+
bson_type = "long"
|
|
118
|
+
elif column.type == "float":
|
|
119
|
+
bson_type = "double"
|
|
120
|
+
elif column.type == "boolean":
|
|
121
|
+
bson_type = "bool"
|
|
122
|
+
elif column.type == "datetime":
|
|
123
|
+
bson_type = "date"
|
|
124
|
+
else:
|
|
125
|
+
raise ValueError(f"Unsupported column type: {column.type}")
|
|
126
|
+
properties[column.name] = {"bsonType": bson_type}
|
|
127
|
+
if not column.is_nullable and not column.is_primary:
|
|
128
|
+
required.append(column.name)
|
|
129
|
+
validator = {"$jsonSchema": {"bsonType": "object", "properties": properties}}
|
|
130
|
+
if required:
|
|
131
|
+
validator["$jsonSchema"]["required"] = required
|
|
132
|
+
if blueprint.name in self.db.list_collection_names():
|
|
133
|
+
return {"status": "exists", "collection": blueprint.name}
|
|
134
|
+
self.db.create_collection(blueprint.name, validator=validator)
|
|
135
|
+
for column in blueprint.columns:
|
|
136
|
+
if column.is_unique or column.is_primary:
|
|
137
|
+
self.db[blueprint.name].create_index(column.name, unique=True)
|
|
138
|
+
elif column.is_index:
|
|
139
|
+
self.db[blueprint.name].create_index(column.name)
|
|
140
|
+
return {"status": "created", "collection": blueprint.name}
|
|
141
|
+
|
|
142
|
+
def schema_drop(self, name):
|
|
143
|
+
if self.db is None:
|
|
144
|
+
self.connect()
|
|
145
|
+
self.db.drop_collection(name)
|
|
146
|
+
return {"status": "dropped", "collection": name}
|
|
147
|
+
|
|
148
|
+
def migrations_get(self):
|
|
149
|
+
if self.db is None:
|
|
150
|
+
self.connect()
|
|
151
|
+
return [doc["name"] for doc in self.db["_migrations"].find().sort([("batch", 1), ("name", 1)])]
|
|
152
|
+
|
|
153
|
+
def migrations_put(self, name, batch):
|
|
154
|
+
if self.db is None:
|
|
155
|
+
self.connect()
|
|
156
|
+
self.db["_migrations"].update_one({"name": name}, {"$set": {"name": name, "batch": batch}}, upsert=True)
|
|
157
|
+
|
|
158
|
+
def migrations_delete(self, name):
|
|
159
|
+
if self.db is None:
|
|
160
|
+
self.connect()
|
|
161
|
+
self.db["_migrations"].delete_one({"name": name})
|
|
162
|
+
|
|
163
|
+
def migrations_max_batch(self):
|
|
164
|
+
if self.db is None:
|
|
165
|
+
self.connect()
|
|
166
|
+
doc = self.db["_migrations"].find_one(sort=[("batch", -1)])
|
|
167
|
+
if doc is None:
|
|
168
|
+
return 0
|
|
169
|
+
return int(doc["batch"])
|
|
170
|
+
|
|
171
|
+
def migrations_batch_for(self, name):
|
|
172
|
+
if self.db is None:
|
|
173
|
+
self.connect()
|
|
174
|
+
doc = self.db["_migrations"].find_one({"name": name})
|
|
175
|
+
if doc is None:
|
|
176
|
+
return None
|
|
177
|
+
return int(doc["batch"])
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from future.databases.Database import Database
|
|
2
|
+
from sqlalchemy import create_engine, text
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MySQL(Database):
|
|
6
|
+
def __init__(self, host: str, port: int, username: str, password: str, database: str):
|
|
7
|
+
super().__init__(host, port, username, password, database)
|
|
8
|
+
self.client = None
|
|
9
|
+
|
|
10
|
+
def connect(self):
|
|
11
|
+
if self.database:
|
|
12
|
+
server = create_engine(f"mysql+pymysql://{self.username}:{self.password}@{self.host}:{self.port}/")
|
|
13
|
+
with server.begin() as connection:
|
|
14
|
+
connection.execute(text(f"CREATE DATABASE IF NOT EXISTS `{self.database}`"))
|
|
15
|
+
server.dispose()
|
|
16
|
+
url = f"mysql+pymysql://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}"
|
|
17
|
+
self.client = create_engine(url)
|
|
18
|
+
return self.client
|
|
19
|
+
|
|
20
|
+
def disconnect(self):
|
|
21
|
+
if self.client is not None:
|
|
22
|
+
self.client.dispose()
|
|
23
|
+
self.client = None
|
|
24
|
+
|
|
25
|
+
def save(self, model):
|
|
26
|
+
if self.client is None:
|
|
27
|
+
self.connect()
|
|
28
|
+
table = model.tableize()
|
|
29
|
+
data = model.to_dict()
|
|
30
|
+
col_sql = ", ".join(f"`{column}`" for column in data.keys())
|
|
31
|
+
val_sql = ", ".join(f":{column}" for column in data.keys())
|
|
32
|
+
upd_sql = ", ".join(f"`{column}`=VALUES(`{column}`)" for column in data.keys() if column != "id")
|
|
33
|
+
if not upd_sql:
|
|
34
|
+
upd_sql = "`id`=`id`"
|
|
35
|
+
insert_sql = text(f"INSERT INTO `{table}` ({col_sql}) VALUES ({val_sql}) ON DUPLICATE KEY UPDATE {upd_sql}")
|
|
36
|
+
with self.client.begin() as connection:
|
|
37
|
+
connection.execute(insert_sql, data)
|
|
38
|
+
return {"status": "saved", "table": table, "id": data.get("id")}
|
|
39
|
+
|
|
40
|
+
def find(self, model, id):
|
|
41
|
+
if self.client is None:
|
|
42
|
+
self.connect()
|
|
43
|
+
table = model.tableize()
|
|
44
|
+
sql = text(f"SELECT * FROM `{table}` WHERE `id` = :id LIMIT 1")
|
|
45
|
+
with self.client.connect() as connection:
|
|
46
|
+
row = connection.execute(sql, {"id": id}).mappings().first()
|
|
47
|
+
if row is None:
|
|
48
|
+
return None
|
|
49
|
+
return model.__class__(**dict(row))
|
|
50
|
+
|
|
51
|
+
def all(self, model):
|
|
52
|
+
if self.client is None:
|
|
53
|
+
self.connect()
|
|
54
|
+
table = model.tableize()
|
|
55
|
+
sql = text(f"SELECT * FROM `{table}`")
|
|
56
|
+
with self.client.connect() as connection:
|
|
57
|
+
rows = connection.execute(sql).mappings().all()
|
|
58
|
+
return [model.__class__(**dict(row)) for row in rows]
|
|
59
|
+
|
|
60
|
+
def get(self, model, wheres, limit=None, orders=None):
|
|
61
|
+
if self.client is None:
|
|
62
|
+
self.connect()
|
|
63
|
+
table = model.tableize()
|
|
64
|
+
clauses = []
|
|
65
|
+
params = {}
|
|
66
|
+
for index, (column, operator, value) in enumerate(wheres):
|
|
67
|
+
if operator == "in":
|
|
68
|
+
values = list(value)
|
|
69
|
+
if not values:
|
|
70
|
+
return []
|
|
71
|
+
keys = []
|
|
72
|
+
for item_index, item in enumerate(values):
|
|
73
|
+
key = f"v{index}_{item_index}"
|
|
74
|
+
keys.append(f":{key}")
|
|
75
|
+
params[key] = item
|
|
76
|
+
clauses.append(f"`{column}` IN ({', '.join(keys)})")
|
|
77
|
+
continue
|
|
78
|
+
if operator == "like":
|
|
79
|
+
key = f"v{index}"
|
|
80
|
+
clauses.append(f"`{column}` LIKE :{key}")
|
|
81
|
+
params[key] = value
|
|
82
|
+
continue
|
|
83
|
+
if operator not in ("=", ">", ">=", "<", "<=", "!="):
|
|
84
|
+
raise ValueError(f"Unsupported operator: {operator}")
|
|
85
|
+
key = f"v{index}"
|
|
86
|
+
clauses.append(f"`{column}` {operator} :{key}")
|
|
87
|
+
params[key] = value
|
|
88
|
+
sql = f"SELECT * FROM `{table}`"
|
|
89
|
+
if clauses:
|
|
90
|
+
sql += f" WHERE {' AND '.join(clauses)}"
|
|
91
|
+
if orders:
|
|
92
|
+
sql += " ORDER BY " + ", ".join(f"`{column}` {direction}" for column, direction in orders)
|
|
93
|
+
if limit is not None:
|
|
94
|
+
sql += f" LIMIT {int(limit)}"
|
|
95
|
+
with self.client.connect() as connection:
|
|
96
|
+
rows = connection.execute(text(sql), params).mappings().all()
|
|
97
|
+
return [model.__class__(**dict(row)) for row in rows]
|
|
98
|
+
|
|
99
|
+
def delete(self, model):
|
|
100
|
+
if self.client is None:
|
|
101
|
+
self.connect()
|
|
102
|
+
table = model.tableize()
|
|
103
|
+
sql = text(f"DELETE FROM `{table}` WHERE `id` = :id")
|
|
104
|
+
with self.client.begin() as connection:
|
|
105
|
+
connection.execute(sql, {"id": getattr(model, "id", None)})
|
|
106
|
+
return {"status": "deleted", "table": table, "id": getattr(model, "id", None)}
|
|
107
|
+
|
|
108
|
+
def update(self, model, changes):
|
|
109
|
+
if self.client is None:
|
|
110
|
+
self.connect()
|
|
111
|
+
table = model.tableize()
|
|
112
|
+
if not changes:
|
|
113
|
+
return {"status": "noop", "table": table}
|
|
114
|
+
set_sql = ", ".join(f"`{column}`=:{column}" for column in changes.keys())
|
|
115
|
+
params = dict(changes)
|
|
116
|
+
params["id"] = getattr(model, "id", None)
|
|
117
|
+
sql = text(f"UPDATE `{table}` SET {set_sql} WHERE `id` = :id")
|
|
118
|
+
with self.client.begin() as connection:
|
|
119
|
+
connection.execute(sql, params)
|
|
120
|
+
return {"status": "updated", "table": table, "id": params["id"]}
|
|
121
|
+
|
|
122
|
+
def schema_create(self, blueprint):
|
|
123
|
+
if self.client is None:
|
|
124
|
+
self.connect()
|
|
125
|
+
definitions = []
|
|
126
|
+
uniques = []
|
|
127
|
+
indexes = []
|
|
128
|
+
for column in blueprint.columns:
|
|
129
|
+
if column.type == "string":
|
|
130
|
+
sql_type = f"VARCHAR({column.length or 255})"
|
|
131
|
+
elif column.type == "text":
|
|
132
|
+
sql_type = "TEXT"
|
|
133
|
+
elif column.type == "integer":
|
|
134
|
+
sql_type = "BIGINT"
|
|
135
|
+
elif column.type == "float":
|
|
136
|
+
sql_type = "DOUBLE"
|
|
137
|
+
elif column.type == "boolean":
|
|
138
|
+
sql_type = "TINYINT(1)"
|
|
139
|
+
elif column.type == "datetime":
|
|
140
|
+
sql_type = "DATETIME"
|
|
141
|
+
else:
|
|
142
|
+
raise ValueError(f"Unsupported column type: {column.type}")
|
|
143
|
+
null_sql = "NULL" if column.is_nullable else "NOT NULL"
|
|
144
|
+
default_sql = ""
|
|
145
|
+
if column.default_value is not None:
|
|
146
|
+
if isinstance(column.default_value, str):
|
|
147
|
+
default_sql = f" DEFAULT '{column.default_value}'"
|
|
148
|
+
else:
|
|
149
|
+
default_sql = f" DEFAULT {column.default_value}"
|
|
150
|
+
primary_sql = " PRIMARY KEY" if column.is_primary else ""
|
|
151
|
+
definitions.append(f"`{column.name}` {sql_type} {null_sql}{default_sql}{primary_sql}")
|
|
152
|
+
if column.is_unique and not column.is_primary:
|
|
153
|
+
uniques.append(f"UNIQUE KEY `unique_{column.name}` (`{column.name}`)")
|
|
154
|
+
if column.is_index and not column.is_primary and not column.is_unique:
|
|
155
|
+
indexes.append(f"KEY `index_{column.name}` (`{column.name}`)")
|
|
156
|
+
parts = definitions + uniques + indexes
|
|
157
|
+
create_sql = text(f"CREATE TABLE IF NOT EXISTS `{blueprint.name}` ({', '.join(parts)})")
|
|
158
|
+
with self.client.begin() as connection:
|
|
159
|
+
connection.execute(create_sql)
|
|
160
|
+
return {"status": "created", "table": blueprint.name}
|
|
161
|
+
|
|
162
|
+
def schema_drop(self, name):
|
|
163
|
+
if self.client is None:
|
|
164
|
+
self.connect()
|
|
165
|
+
with self.client.begin() as connection:
|
|
166
|
+
connection.execute(text(f"DROP TABLE IF EXISTS `{name}`"))
|
|
167
|
+
return {"status": "dropped", "table": name}
|
|
168
|
+
|
|
169
|
+
def table_exists(self, name):
|
|
170
|
+
if self.client is None:
|
|
171
|
+
self.connect()
|
|
172
|
+
sql = text("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :name LIMIT 1")
|
|
173
|
+
with self.client.connect() as connection:
|
|
174
|
+
return connection.execute(sql, {"name": name}).first() is not None
|
|
175
|
+
|
|
176
|
+
def _ensure_migrations_table(self):
|
|
177
|
+
if self.client is None:
|
|
178
|
+
self.connect()
|
|
179
|
+
sql = text("CREATE TABLE IF NOT EXISTS `migrations` (`name` VARCHAR(255) PRIMARY KEY, `batch` INT NOT NULL)")
|
|
180
|
+
with self.client.begin() as connection:
|
|
181
|
+
connection.execute(sql)
|
|
182
|
+
|
|
183
|
+
def migrations_get(self):
|
|
184
|
+
self._ensure_migrations_table()
|
|
185
|
+
with self.client.connect() as connection:
|
|
186
|
+
rows = connection.execute(text("SELECT `name` FROM `migrations` ORDER BY `batch`, `name`")).mappings().all()
|
|
187
|
+
return [row["name"] for row in rows]
|
|
188
|
+
|
|
189
|
+
def migrations_put(self, name, batch):
|
|
190
|
+
self._ensure_migrations_table()
|
|
191
|
+
sql = text("INSERT INTO `migrations` (`name`, `batch`) VALUES (:name, :batch)")
|
|
192
|
+
with self.client.begin() as connection:
|
|
193
|
+
connection.execute(sql, {"name": name, "batch": batch})
|
|
194
|
+
|
|
195
|
+
def migrations_delete(self, name):
|
|
196
|
+
self._ensure_migrations_table()
|
|
197
|
+
sql = text("DELETE FROM `migrations` WHERE `name` = :name")
|
|
198
|
+
with self.client.begin() as connection:
|
|
199
|
+
connection.execute(sql, {"name": name})
|
|
200
|
+
|
|
201
|
+
def migrations_max_batch(self):
|
|
202
|
+
self._ensure_migrations_table()
|
|
203
|
+
with self.client.connect() as connection:
|
|
204
|
+
row = connection.execute(text("SELECT MAX(`batch`) AS `batch` FROM `migrations`")).mappings().first()
|
|
205
|
+
if row is None or row["batch"] is None:
|
|
206
|
+
return 0
|
|
207
|
+
return int(row["batch"])
|
|
208
|
+
|
|
209
|
+
def migrations_batch_for(self, name):
|
|
210
|
+
self._ensure_migrations_table()
|
|
211
|
+
with self.client.connect() as connection:
|
|
212
|
+
row = connection.execute(text("SELECT `batch` FROM `migrations` WHERE `name` = :name"), {"name": name}).mappings().first()
|
|
213
|
+
if row is None:
|
|
214
|
+
return None
|
|
215
|
+
return int(row["batch"])
|