future-framework 2.1.1__tar.gz → 3.0.1__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.
- {future_framework-2.1.1 → future_framework-3.0.1}/PKG-INFO +1 -1
- {future_framework-2.1.1 → future_framework-3.0.1}/future/databases/ClickhouseDatabase.py +48 -17
- {future_framework-2.1.1 → future_framework-3.0.1}/future/databases/ElasticsearchDatabase.py +36 -4
- {future_framework-2.1.1 → future_framework-3.0.1}/future/databases/MongoDBDatabase.py +26 -4
- {future_framework-2.1.1 → future_framework-3.0.1}/future/databases/MySQLDatabase.py +48 -4
- {future_framework-2.1.1 → future_framework-3.0.1}/future/databases/PostgresDatabase.py +49 -4
- {future_framework-2.1.1 → future_framework-3.0.1}/future/databases/RedisDatabase.py +67 -7
- {future_framework-2.1.1 → future_framework-3.0.1}/future/databases/SQLiteDatabase.py +51 -4
- {future_framework-2.1.1 → future_framework-3.0.1}/future/interfaces/IDatabase.py +3 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/migrations.py +80 -10
- {future_framework-2.1.1 → future_framework-3.0.1}/pyproject.toml +1 -1
- {future_framework-2.1.1 → future_framework-3.0.1}/LICENSE +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/README.md +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/__init__.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/application.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/Auth0Authentication.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/AzureADAuthentication.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/BasicAuthentication.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/KerberosAuthentication.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/KeycloakAuthentication.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/OAuth2Authentication.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/OpenIdConnectAuthentication.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/SAMLAuthentication.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/__init__.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/cli.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/controllers/DebugController.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/controllers/GraphQLController.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/controllers/OpenAPIController.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/controllers/WebSocketController.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/controllers/WelcomeController.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/controllers/__init__.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/database.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/databases/__init__.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/exceptions.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/interfaces/IAuthentication.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/interfaces/IController.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/interfaces/IMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/interfaces/IModel.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/interfaces/IPlugin.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/interfaces/ITask.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/interfaces/__init__.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/interfacing.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/lifespan.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/logger.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/BruteforcePreventionMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/CORSMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/CSRFMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/GZipMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/HTAccessConfuserMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/HeaderConfuserMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/RateLimitMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/ResponseCodeConfuserMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/SQLiConfuserMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/ScopeValidationMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/SessionMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/TestMiddlewareRequest.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/TestMiddlewareResponse.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/WebServerConfuserMiddleware.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/__init__.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/models/PostModel.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/models/Query.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/models/UserModel.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/models/__init__.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/openapi.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/plugins/ElasticsearchPlugin.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/plugins/__init__.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/request.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/response.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/routing.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/seeder.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/settings.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/tasks/CheckDNSTask.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/tasks/CheckDiskUsageTask.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/tasks/CheckHttpStatusTask.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/tasks/CheckMemoryUsageTask.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/tasks/CheckSSHBannerTask.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/tasks/CheckSystemUptimeTask.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/tasks/DailyBackupTask.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/tasks/SyncTaskExample.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/tasks/__init__.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/taskscheduler.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/testclient.py +0 -0
- {future_framework-2.1.1 → future_framework-3.0.1}/future/types.py +0 -0
|
@@ -123,23 +123,7 @@ class ClickhouseDatabase(IDatabase):
|
|
|
123
123
|
definitions = []
|
|
124
124
|
order_by = "tuple()"
|
|
125
125
|
for column in blueprint.columns:
|
|
126
|
-
|
|
127
|
-
ch_type = "String"
|
|
128
|
-
elif column.type == "text":
|
|
129
|
-
ch_type = "String"
|
|
130
|
-
elif column.type == "integer":
|
|
131
|
-
ch_type = "Int64"
|
|
132
|
-
elif column.type == "float":
|
|
133
|
-
ch_type = "Float64"
|
|
134
|
-
elif column.type == "boolean":
|
|
135
|
-
ch_type = "UInt8"
|
|
136
|
-
elif column.type == "datetime":
|
|
137
|
-
ch_type = "DateTime"
|
|
138
|
-
else:
|
|
139
|
-
raise ValueError(f"Unsupported column type: {column.type}")
|
|
140
|
-
if column.is_nullable:
|
|
141
|
-
ch_type = f"Nullable({ch_type})"
|
|
142
|
-
definitions.append(f"`{column.name}` {ch_type}")
|
|
126
|
+
definitions.append(self._column_definition(column))
|
|
143
127
|
if column.is_primary:
|
|
144
128
|
order_by = f"`{column.name}`"
|
|
145
129
|
create_sql = f"CREATE TABLE IF NOT EXISTS `{blueprint.name}` ({', '.join(definitions)}) ENGINE = MergeTree() ORDER BY {order_by}"
|
|
@@ -147,6 +131,53 @@ class ClickhouseDatabase(IDatabase):
|
|
|
147
131
|
await cursor.execute(create_sql)
|
|
148
132
|
return {"status": "created", "table": blueprint.name}
|
|
149
133
|
|
|
134
|
+
def _column_type(self, column):
|
|
135
|
+
if column.type in ("string", "text"):
|
|
136
|
+
ch_type = "String"
|
|
137
|
+
elif column.type == "integer":
|
|
138
|
+
ch_type = "Int64"
|
|
139
|
+
elif column.type == "float":
|
|
140
|
+
ch_type = "Float64"
|
|
141
|
+
elif column.type == "boolean":
|
|
142
|
+
ch_type = "UInt8"
|
|
143
|
+
elif column.type == "datetime":
|
|
144
|
+
ch_type = "DateTime"
|
|
145
|
+
else:
|
|
146
|
+
raise ValueError(f"Unsupported column type: {column.type}")
|
|
147
|
+
return f"Nullable({ch_type})" if column.is_nullable else ch_type
|
|
148
|
+
|
|
149
|
+
def _column_definition(self, column):
|
|
150
|
+
default = f" DEFAULT {column.default_value!r}" if column.default_value is not None else ""
|
|
151
|
+
return f"`{column.name}` {self._column_type(column)}{default}"
|
|
152
|
+
|
|
153
|
+
async def table_exists(self, name):
|
|
154
|
+
if self.client is None:
|
|
155
|
+
await self.connect()
|
|
156
|
+
async with self.client.cursor() as cursor:
|
|
157
|
+
await cursor.execute("SELECT count() FROM system.tables WHERE database = currentDatabase() AND name = %(name)s", {"name": name})
|
|
158
|
+
rows = await cursor.fetchall()
|
|
159
|
+
return bool(rows and rows[0][0])
|
|
160
|
+
|
|
161
|
+
async def schema_update(self, blueprint):
|
|
162
|
+
if self.client is None:
|
|
163
|
+
await self.connect()
|
|
164
|
+
if not await self.table_exists(blueprint.name):
|
|
165
|
+
return await self.schema_create(blueprint)
|
|
166
|
+
async with self.client.cursor() as cursor:
|
|
167
|
+
await cursor.execute("SELECT name, type FROM system.columns WHERE database = currentDatabase() AND table = %(table)s", {"table": blueprint.name})
|
|
168
|
+
rows = await cursor.fetchall()
|
|
169
|
+
existing = {row[0]: row[1] for row in rows}
|
|
170
|
+
desired = {column.name: column for column in blueprint.columns}
|
|
171
|
+
for name, column in desired.items():
|
|
172
|
+
definition = self._column_definition(column)
|
|
173
|
+
if name not in existing:
|
|
174
|
+
await cursor.execute(f"ALTER TABLE `{blueprint.name}` ADD COLUMN {definition}")
|
|
175
|
+
elif existing[name] != self._column_type(column):
|
|
176
|
+
await cursor.execute(f"ALTER TABLE `{blueprint.name}` MODIFY COLUMN {definition}")
|
|
177
|
+
for name in existing.keys() - desired.keys():
|
|
178
|
+
await cursor.execute(f"ALTER TABLE `{blueprint.name}` DROP COLUMN `{name}`")
|
|
179
|
+
return {"status": "updated", "table": blueprint.name, "columns": sorted(desired)}
|
|
180
|
+
|
|
150
181
|
async def schema_drop(self, name):
|
|
151
182
|
if self.client is None:
|
|
152
183
|
await self.connect()
|
|
@@ -115,6 +115,13 @@ class ElasticsearchDatabase(IDatabase):
|
|
|
115
115
|
async def schema_create(self, blueprint):
|
|
116
116
|
if self.client is None:
|
|
117
117
|
await self.connect()
|
|
118
|
+
properties = self._mapping_properties(blueprint)
|
|
119
|
+
if await self.client.indices.exists(index=blueprint.name):
|
|
120
|
+
return {"status": "exists", "index": blueprint.name}
|
|
121
|
+
result = await self.client.indices.create(index=blueprint.name, mappings={"properties": properties})
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
def _mapping_properties(self, blueprint):
|
|
118
125
|
properties = {}
|
|
119
126
|
for column in blueprint.columns:
|
|
120
127
|
if column.type == "string" and (column.is_primary or column.name == "id"):
|
|
@@ -133,10 +140,35 @@ class ElasticsearchDatabase(IDatabase):
|
|
|
133
140
|
properties[column.name] = {"type": "date"}
|
|
134
141
|
else:
|
|
135
142
|
raise ValueError(f"Unsupported column type: {column.type}")
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
return properties
|
|
144
|
+
|
|
145
|
+
async def schema_update(self, blueprint):
|
|
146
|
+
if self.client is None:
|
|
147
|
+
await self.connect()
|
|
148
|
+
if not await self.client.indices.exists(index=blueprint.name):
|
|
149
|
+
return await self.schema_create(blueprint)
|
|
150
|
+
desired = self._mapping_properties(blueprint)
|
|
151
|
+
mapping = await self.client.indices.get_mapping(index=blueprint.name)
|
|
152
|
+
current = mapping.get(blueprint.name, {}).get("mappings", {}).get("properties", {})
|
|
153
|
+
if current == desired:
|
|
154
|
+
return {"status": "unchanged", "index": blueprint.name}
|
|
155
|
+
additive = set(current).issubset(desired) and all(current[name] == desired[name] for name in current)
|
|
156
|
+
if additive:
|
|
157
|
+
await self.client.indices.put_mapping(index=blueprint.name, properties=desired)
|
|
158
|
+
return {"status": "updated", "index": blueprint.name, "properties": sorted(desired)}
|
|
159
|
+
|
|
160
|
+
temporary = f"_future_migrate_{blueprint.name}"
|
|
161
|
+
if await self.client.indices.exists(index=temporary):
|
|
162
|
+
await self.client.indices.delete(index=temporary)
|
|
163
|
+
await self.client.indices.create(index=temporary, mappings={"dynamic": False, "properties": desired})
|
|
164
|
+
keep = list(desired)
|
|
165
|
+
script = {"source": "ctx._source.keySet().removeIf(k -> !params.keep.contains(k))", "params": {"keep": keep}}
|
|
166
|
+
await self.client.reindex(body={"source": {"index": blueprint.name}, "dest": {"index": temporary}, "script": script}, wait_for_completion=True, refresh=True)
|
|
167
|
+
await self.client.indices.delete(index=blueprint.name)
|
|
168
|
+
await self.client.indices.create(index=blueprint.name, mappings={"dynamic": False, "properties": desired})
|
|
169
|
+
await self.client.reindex(body={"source": {"index": temporary}, "dest": {"index": blueprint.name}}, wait_for_completion=True, refresh=True)
|
|
170
|
+
await self.client.indices.delete(index=temporary)
|
|
171
|
+
return {"status": "updated", "index": blueprint.name, "properties": sorted(desired)}
|
|
140
172
|
|
|
141
173
|
async def schema_drop(self, name):
|
|
142
174
|
if self.client is None:
|
|
@@ -108,6 +108,14 @@ class MongoDBDatabase(IDatabase):
|
|
|
108
108
|
async def schema_create(self, blueprint):
|
|
109
109
|
if self.db is None:
|
|
110
110
|
await self.connect()
|
|
111
|
+
validator = self._validator(blueprint)
|
|
112
|
+
if blueprint.name in await self.db.list_collection_names():
|
|
113
|
+
return {"status": "exists", "collection": blueprint.name}
|
|
114
|
+
await self.db.create_collection(blueprint.name, validator=validator)
|
|
115
|
+
await self._ensure_indexes(blueprint)
|
|
116
|
+
return {"status": "created", "collection": blueprint.name}
|
|
117
|
+
|
|
118
|
+
def _validator(self, blueprint):
|
|
111
119
|
properties = {}
|
|
112
120
|
required = []
|
|
113
121
|
for column in blueprint.columns:
|
|
@@ -129,15 +137,29 @@ class MongoDBDatabase(IDatabase):
|
|
|
129
137
|
validator = {"$jsonSchema": {"bsonType": "object", "properties": properties}}
|
|
130
138
|
if required:
|
|
131
139
|
validator["$jsonSchema"]["required"] = required
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
return validator
|
|
141
|
+
|
|
142
|
+
async def _ensure_indexes(self, blueprint):
|
|
135
143
|
for column in blueprint.columns:
|
|
136
144
|
if column.is_unique or column.is_primary:
|
|
137
145
|
await self.db[blueprint.name].create_index(column.name, unique=True)
|
|
138
146
|
elif column.is_index:
|
|
139
147
|
await self.db[blueprint.name].create_index(column.name)
|
|
140
|
-
|
|
148
|
+
|
|
149
|
+
async def table_exists(self, name):
|
|
150
|
+
if self.db is None:
|
|
151
|
+
await self.connect()
|
|
152
|
+
return name in await self.db.list_collection_names()
|
|
153
|
+
|
|
154
|
+
async def schema_update(self, blueprint):
|
|
155
|
+
if self.db is None:
|
|
156
|
+
await self.connect()
|
|
157
|
+
if not await self.table_exists(blueprint.name):
|
|
158
|
+
return await self.schema_create(blueprint)
|
|
159
|
+
validator = self._validator(blueprint)
|
|
160
|
+
await self.db.command({"collMod": blueprint.name, "validator": validator})
|
|
161
|
+
await self._ensure_indexes(blueprint)
|
|
162
|
+
return {"status": "updated", "collection": blueprint.name, "properties": sorted(column.name for column in blueprint.columns)}
|
|
141
163
|
|
|
142
164
|
async def schema_drop(self, name):
|
|
143
165
|
if self.db is None:
|
|
@@ -123,6 +123,13 @@ class MySQLDatabase(IDatabase):
|
|
|
123
123
|
async def schema_create(self, blueprint):
|
|
124
124
|
if self.client is None:
|
|
125
125
|
await self.connect()
|
|
126
|
+
parts = self._schema_parts(blueprint)
|
|
127
|
+
create_sql = text(f"CREATE TABLE IF NOT EXISTS `{blueprint.name}` ({', '.join(parts)})")
|
|
128
|
+
async with self.client.begin() as connection:
|
|
129
|
+
await connection.execute(create_sql)
|
|
130
|
+
return {"status": "created", "table": blueprint.name}
|
|
131
|
+
|
|
132
|
+
def _schema_parts(self, blueprint):
|
|
126
133
|
definitions = []
|
|
127
134
|
uniques = []
|
|
128
135
|
indexes = []
|
|
@@ -154,11 +161,48 @@ class MySQLDatabase(IDatabase):
|
|
|
154
161
|
uniques.append(f"UNIQUE KEY `unique_{column.name}` (`{column.name}`)")
|
|
155
162
|
if column.is_index and not column.is_primary and not column.is_unique:
|
|
156
163
|
indexes.append(f"KEY `index_{column.name}` (`{column.name}`)")
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
return definitions + uniques + indexes
|
|
165
|
+
|
|
166
|
+
async def schema_update(self, blueprint):
|
|
167
|
+
if self.client is None:
|
|
168
|
+
await self.connect()
|
|
169
|
+
if not await self.table_exists(blueprint.name):
|
|
170
|
+
return await self.schema_create(blueprint)
|
|
171
|
+
sql = text("SELECT COLUMN_NAME FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = :table ORDER BY ORDINAL_POSITION")
|
|
172
|
+
async with self.client.connect() as connection:
|
|
173
|
+
rows = (await connection.execute(sql, {"table": blueprint.name})).mappings().all()
|
|
174
|
+
existing = {row["COLUMN_NAME"] for row in rows}
|
|
175
|
+
temporary = f"_future_migrate_{blueprint.name}"
|
|
176
|
+
backup = f"_future_backup_{blueprint.name}"
|
|
177
|
+
parts = self._schema_parts(blueprint)
|
|
159
178
|
async with self.client.begin() as connection:
|
|
160
|
-
await connection.execute(
|
|
161
|
-
|
|
179
|
+
await connection.execute(text(f"DROP TABLE IF EXISTS `{temporary}`"))
|
|
180
|
+
await connection.execute(text(f"DROP TABLE IF EXISTS `{backup}`"))
|
|
181
|
+
await connection.execute(text(f"CREATE TABLE `{temporary}` ({', '.join(parts)})"))
|
|
182
|
+
targets = []
|
|
183
|
+
sources = []
|
|
184
|
+
backfills = {}
|
|
185
|
+
for index, column in enumerate(blueprint.columns):
|
|
186
|
+
quoted = f"`{column.name}`"
|
|
187
|
+
if column.name in existing:
|
|
188
|
+
source = quoted
|
|
189
|
+
if not column.is_nullable:
|
|
190
|
+
key = f"future_backfill_{index}"
|
|
191
|
+
source = f"COALESCE({quoted}, :{key})"
|
|
192
|
+
backfills[key] = column.backfill_value()
|
|
193
|
+
elif not column.is_nullable:
|
|
194
|
+
key = f"future_backfill_{index}"
|
|
195
|
+
source = f":{key}"
|
|
196
|
+
backfills[key] = column.backfill_value()
|
|
197
|
+
else:
|
|
198
|
+
continue
|
|
199
|
+
targets.append(quoted)
|
|
200
|
+
sources.append(source)
|
|
201
|
+
if targets:
|
|
202
|
+
await connection.execute(text(f"INSERT INTO `{temporary}` ({', '.join(targets)}) SELECT {', '.join(sources)} FROM `{blueprint.name}`"), backfills)
|
|
203
|
+
await connection.execute(text(f"RENAME TABLE `{blueprint.name}` TO `{backup}`, `{temporary}` TO `{blueprint.name}`"))
|
|
204
|
+
await connection.execute(text(f"DROP TABLE `{backup}`"))
|
|
205
|
+
return {"status": "updated", "table": blueprint.name, "columns": sorted(column.name for column in blueprint.columns)}
|
|
162
206
|
|
|
163
207
|
async def schema_drop(self, name):
|
|
164
208
|
if self.client is None:
|
|
@@ -3,6 +3,7 @@ from sqlalchemy import BigInteger, String, bindparam, text
|
|
|
3
3
|
from sqlalchemy.dialects.postgresql import ARRAY
|
|
4
4
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
5
5
|
from urllib.parse import quote_plus
|
|
6
|
+
from uuid import uuid4
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class PostgresDatabase(IDatabase):
|
|
@@ -128,6 +129,15 @@ class PostgresDatabase(IDatabase):
|
|
|
128
129
|
async def schema_create(self, blueprint):
|
|
129
130
|
if self.client is None:
|
|
130
131
|
await self.connect()
|
|
132
|
+
parts, indexes = self._schema_parts(blueprint)
|
|
133
|
+
create_sql = text(f"CREATE TABLE IF NOT EXISTS \"{blueprint.name}\" ({', '.join(parts)})")
|
|
134
|
+
async with self.client.begin() as connection:
|
|
135
|
+
await connection.execute(create_sql)
|
|
136
|
+
for index_name in indexes:
|
|
137
|
+
await connection.execute(text(f"CREATE INDEX IF NOT EXISTS \"index_{blueprint.name}_{index_name}\" ON \"{blueprint.name}\" (\"{index_name}\")"))
|
|
138
|
+
return {"status": "created", "table": blueprint.name}
|
|
139
|
+
|
|
140
|
+
def _schema_parts(self, blueprint):
|
|
131
141
|
definitions = []
|
|
132
142
|
uniques = []
|
|
133
143
|
indexes = []
|
|
@@ -159,13 +169,48 @@ class PostgresDatabase(IDatabase):
|
|
|
159
169
|
uniques.append(f"UNIQUE (\"{column.name}\")")
|
|
160
170
|
if column.is_index and not column.is_primary and not column.is_unique:
|
|
161
171
|
indexes.append(column.name)
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
return definitions + uniques, indexes
|
|
173
|
+
|
|
174
|
+
async def schema_update(self, blueprint):
|
|
175
|
+
if self.client is None:
|
|
176
|
+
await self.connect()
|
|
177
|
+
if not await self.table_exists(blueprint.name):
|
|
178
|
+
return await self.schema_create(blueprint)
|
|
179
|
+
sql = text("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = :table ORDER BY ordinal_position")
|
|
180
|
+
async with self.client.connect() as connection:
|
|
181
|
+
rows = (await connection.execute(sql, {"table": blueprint.name})).mappings().all()
|
|
182
|
+
existing = {row["column_name"] for row in rows}
|
|
183
|
+
temporary = f"_future_{uuid4().hex[:8]}_{blueprint.name}"[:63]
|
|
184
|
+
parts, indexes = self._schema_parts(blueprint)
|
|
164
185
|
async with self.client.begin() as connection:
|
|
165
|
-
await connection.execute(
|
|
186
|
+
await connection.execute(text(f'DROP TABLE IF EXISTS "{temporary}"'))
|
|
187
|
+
await connection.execute(text(f'CREATE TABLE "{temporary}" ({", ".join(parts)})'))
|
|
188
|
+
targets = []
|
|
189
|
+
sources = []
|
|
190
|
+
backfills = {}
|
|
191
|
+
for index, column in enumerate(blueprint.columns):
|
|
192
|
+
quoted = f'"{column.name}"'
|
|
193
|
+
if column.name in existing:
|
|
194
|
+
source = quoted
|
|
195
|
+
if not column.is_nullable:
|
|
196
|
+
key = f"future_backfill_{index}"
|
|
197
|
+
source = f"COALESCE({quoted}, :{key})"
|
|
198
|
+
backfills[key] = column.backfill_value()
|
|
199
|
+
elif not column.is_nullable:
|
|
200
|
+
key = f"future_backfill_{index}"
|
|
201
|
+
source = f":{key}"
|
|
202
|
+
backfills[key] = column.backfill_value()
|
|
203
|
+
else:
|
|
204
|
+
continue
|
|
205
|
+
targets.append(quoted)
|
|
206
|
+
sources.append(source)
|
|
207
|
+
if targets:
|
|
208
|
+
await connection.execute(text(f'INSERT INTO "{temporary}" ({", ".join(targets)}) SELECT {", ".join(sources)} FROM "{blueprint.name}"'), backfills)
|
|
209
|
+
await connection.execute(text(f'DROP TABLE "{blueprint.name}"'))
|
|
210
|
+
await connection.execute(text(f'ALTER TABLE "{temporary}" RENAME TO "{blueprint.name}"'))
|
|
166
211
|
for index_name in indexes:
|
|
167
212
|
await connection.execute(text(f"CREATE INDEX IF NOT EXISTS \"index_{blueprint.name}_{index_name}\" ON \"{blueprint.name}\" (\"{index_name}\")"))
|
|
168
|
-
return {"status": "
|
|
213
|
+
return {"status": "updated", "table": blueprint.name, "columns": sorted(column.name for column in blueprint.columns)}
|
|
169
214
|
|
|
170
215
|
async def schema_drop(self, name):
|
|
171
216
|
if self.client is None:
|
|
@@ -27,6 +27,12 @@ class RedisDatabase(IDatabase):
|
|
|
27
27
|
def _ids_key(self, table):
|
|
28
28
|
return f"{table}:ids"
|
|
29
29
|
|
|
30
|
+
def _schema_key(self, table):
|
|
31
|
+
return f"future:schema:{table}"
|
|
32
|
+
|
|
33
|
+
def _migrations_key(self):
|
|
34
|
+
return "future:migrations"
|
|
35
|
+
|
|
30
36
|
async def save(self, model):
|
|
31
37
|
if self.client is None:
|
|
32
38
|
await self.connect()
|
|
@@ -125,22 +131,76 @@ class RedisDatabase(IDatabase):
|
|
|
125
131
|
return {"status": "updated", "id": doc_id}
|
|
126
132
|
|
|
127
133
|
async def schema_create(self, blueprint):
|
|
128
|
-
|
|
134
|
+
if self.client is None:
|
|
135
|
+
await self.connect()
|
|
136
|
+
schema = self._schema_document(blueprint)
|
|
137
|
+
created = await self.client.set(self._schema_key(blueprint.name), json.dumps(schema, sort_keys=True, default=str), nx=True)
|
|
138
|
+
return {"status": "created" if created else "exists", "table": blueprint.name}
|
|
139
|
+
|
|
140
|
+
def _schema_document(self, blueprint):
|
|
141
|
+
return {
|
|
142
|
+
"name": blueprint.name,
|
|
143
|
+
"columns": [
|
|
144
|
+
{
|
|
145
|
+
"name": column.name,
|
|
146
|
+
"type": column.type,
|
|
147
|
+
"length": column.length,
|
|
148
|
+
"nullable": column.is_nullable,
|
|
149
|
+
"unique": column.is_unique,
|
|
150
|
+
"primary": column.is_primary,
|
|
151
|
+
"index": column.is_index,
|
|
152
|
+
"default": column.default_value,
|
|
153
|
+
}
|
|
154
|
+
for column in blueprint.columns
|
|
155
|
+
],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async def table_exists(self, name):
|
|
159
|
+
if self.client is None:
|
|
160
|
+
await self.connect()
|
|
161
|
+
return bool(await self.client.exists(self._schema_key(name)))
|
|
162
|
+
|
|
163
|
+
async def schema_update(self, blueprint):
|
|
164
|
+
if self.client is None:
|
|
165
|
+
await self.connect()
|
|
166
|
+
await self.client.set(self._schema_key(blueprint.name), json.dumps(self._schema_document(blueprint), sort_keys=True, default=str))
|
|
167
|
+
return {"status": "updated", "table": blueprint.name, "columns": sorted(column.name for column in blueprint.columns)}
|
|
129
168
|
|
|
130
169
|
async def schema_drop(self, name):
|
|
131
|
-
|
|
170
|
+
if self.client is None:
|
|
171
|
+
await self.connect()
|
|
172
|
+
ids = await self.client.smembers(self._ids_key(name))
|
|
173
|
+
keys = [self._key(name, doc_id) for doc_id in ids]
|
|
174
|
+
keys.extend([self._ids_key(name), self._schema_key(name)])
|
|
175
|
+
if keys:
|
|
176
|
+
await self.client.delete(*keys)
|
|
177
|
+
return {"status": "dropped", "table": name}
|
|
132
178
|
|
|
133
179
|
async def migrations_get(self):
|
|
134
|
-
|
|
180
|
+
if self.client is None:
|
|
181
|
+
await self.connect()
|
|
182
|
+
return list(await self.client.zrange(self._migrations_key(), 0, -1))
|
|
135
183
|
|
|
136
184
|
async def migrations_put(self, name, batch):
|
|
137
|
-
|
|
185
|
+
if self.client is None:
|
|
186
|
+
await self.connect()
|
|
187
|
+
await self.client.zadd(self._migrations_key(), {name: int(batch)})
|
|
138
188
|
|
|
139
189
|
async def migrations_delete(self, name):
|
|
140
|
-
|
|
190
|
+
if self.client is None:
|
|
191
|
+
await self.connect()
|
|
192
|
+
await self.client.zrem(self._migrations_key(), name)
|
|
141
193
|
|
|
142
194
|
async def migrations_max_batch(self):
|
|
143
|
-
|
|
195
|
+
if self.client is None:
|
|
196
|
+
await self.connect()
|
|
197
|
+
rows = await self.client.zrevrange(self._migrations_key(), 0, 0, withscores=True)
|
|
198
|
+
if not rows:
|
|
199
|
+
return 0
|
|
200
|
+
return int(rows[0][1])
|
|
144
201
|
|
|
145
202
|
async def migrations_batch_for(self, name):
|
|
146
|
-
|
|
203
|
+
if self.client is None:
|
|
204
|
+
await self.connect()
|
|
205
|
+
score = await self.client.zscore(self._migrations_key(), name)
|
|
206
|
+
return None if score is None else int(score)
|
|
@@ -128,6 +128,15 @@ class SQLiteDatabase(IDatabase):
|
|
|
128
128
|
async def schema_create(self, blueprint):
|
|
129
129
|
if self.client is None:
|
|
130
130
|
await self.connect()
|
|
131
|
+
definitions, indexes = self._schema_parts(blueprint)
|
|
132
|
+
create_sql = text(f'CREATE TABLE IF NOT EXISTS "{blueprint.name}" ({", ".join(definitions)})')
|
|
133
|
+
async with self.client.begin() as connection:
|
|
134
|
+
await connection.execute(create_sql)
|
|
135
|
+
for index_sql in indexes:
|
|
136
|
+
await connection.execute(text(index_sql))
|
|
137
|
+
return {"status": "created", "table": blueprint.name}
|
|
138
|
+
|
|
139
|
+
def _schema_parts(self, blueprint, index_table=None):
|
|
131
140
|
definitions = []
|
|
132
141
|
indexes = []
|
|
133
142
|
for column in blueprint.columns:
|
|
@@ -156,13 +165,51 @@ class SQLiteDatabase(IDatabase):
|
|
|
156
165
|
unique_sql = " UNIQUE" if column.is_unique and not column.is_primary else ""
|
|
157
166
|
definitions.append(f'"{column.name}" {sql_type} {null_sql}{default_sql}{primary_sql}{unique_sql}')
|
|
158
167
|
if column.is_index and not column.is_primary and not column.is_unique:
|
|
159
|
-
|
|
160
|
-
|
|
168
|
+
table = index_table or blueprint.name
|
|
169
|
+
indexes.append(f'CREATE INDEX IF NOT EXISTS "index_{table}_{column.name}" ON "{table}" ("{column.name}")')
|
|
170
|
+
return definitions, indexes
|
|
171
|
+
|
|
172
|
+
async def schema_update(self, blueprint):
|
|
173
|
+
if self.client is None:
|
|
174
|
+
await self.connect()
|
|
175
|
+
if not await self.table_exists(blueprint.name):
|
|
176
|
+
return await self.schema_create(blueprint)
|
|
177
|
+
async with self.client.connect() as connection:
|
|
178
|
+
rows = (await connection.execute(text(f'PRAGMA table_info("{blueprint.name}")'))).mappings().all()
|
|
179
|
+
existing = {row["name"] for row in rows}
|
|
180
|
+
desired = {column.name for column in blueprint.columns}
|
|
181
|
+
temporary = f"_future_migrate_{blueprint.name}"
|
|
182
|
+
definitions, _ = self._schema_parts(blueprint)
|
|
183
|
+
_, indexes = self._schema_parts(blueprint, index_table=blueprint.name)
|
|
161
184
|
async with self.client.begin() as connection:
|
|
162
|
-
await connection.execute(
|
|
185
|
+
await connection.execute(text(f'DROP TABLE IF EXISTS "{temporary}"'))
|
|
186
|
+
await connection.execute(text(f'CREATE TABLE "{temporary}" ({", ".join(definitions)})'))
|
|
187
|
+
targets = []
|
|
188
|
+
sources = []
|
|
189
|
+
backfills = {}
|
|
190
|
+
for index, column in enumerate(blueprint.columns):
|
|
191
|
+
quoted = f'"{column.name}"'
|
|
192
|
+
if column.name in existing:
|
|
193
|
+
source = quoted
|
|
194
|
+
if not column.is_nullable:
|
|
195
|
+
key = f"future_backfill_{index}"
|
|
196
|
+
source = f"COALESCE({quoted}, :{key})"
|
|
197
|
+
backfills[key] = column.backfill_value()
|
|
198
|
+
elif not column.is_nullable:
|
|
199
|
+
key = f"future_backfill_{index}"
|
|
200
|
+
source = f":{key}"
|
|
201
|
+
backfills[key] = column.backfill_value()
|
|
202
|
+
else:
|
|
203
|
+
continue
|
|
204
|
+
targets.append(quoted)
|
|
205
|
+
sources.append(source)
|
|
206
|
+
if targets:
|
|
207
|
+
await connection.execute(text(f'INSERT INTO "{temporary}" ({", ".join(targets)}) SELECT {", ".join(sources)} FROM "{blueprint.name}"'), backfills)
|
|
208
|
+
await connection.execute(text(f'DROP TABLE "{blueprint.name}"'))
|
|
209
|
+
await connection.execute(text(f'ALTER TABLE "{temporary}" RENAME TO "{blueprint.name}"'))
|
|
163
210
|
for index_sql in indexes:
|
|
164
211
|
await connection.execute(text(index_sql))
|
|
165
|
-
return {"status": "
|
|
212
|
+
return {"status": "updated", "table": blueprint.name, "columns": sorted(desired)}
|
|
166
213
|
|
|
167
214
|
async def schema_drop(self, name):
|
|
168
215
|
if self.client is None:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Migrations — Orator/Masonite-style schema, discovery, and generation.
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
import ast
|
|
4
5
|
import importlib.util
|
|
5
6
|
import inflection
|
|
6
7
|
|
|
@@ -54,6 +55,21 @@ class Column:
|
|
|
54
55
|
self.default_value = value
|
|
55
56
|
return self
|
|
56
57
|
|
|
58
|
+
def backfill_value(self):
|
|
59
|
+
if self.default_value is not None:
|
|
60
|
+
return self.default_value
|
|
61
|
+
if self.type in ("string", "text"):
|
|
62
|
+
return ""
|
|
63
|
+
if self.type == "integer":
|
|
64
|
+
return 0
|
|
65
|
+
if self.type == "float":
|
|
66
|
+
return 0.0
|
|
67
|
+
if self.type == "boolean":
|
|
68
|
+
return False
|
|
69
|
+
if self.type == "datetime":
|
|
70
|
+
return datetime(1970, 1, 1)
|
|
71
|
+
raise ValueError(f"Unsupported column type: {self.type}")
|
|
72
|
+
|
|
57
73
|
|
|
58
74
|
class Blueprint:
|
|
59
75
|
def __init__(self, name, connection_name, action="create"):
|
|
@@ -71,6 +87,8 @@ class Blueprint:
|
|
|
71
87
|
connection = Database().get_connection(self.connection_name)
|
|
72
88
|
if self.action == "create":
|
|
73
89
|
await connection.schema_create(self)
|
|
90
|
+
elif self.action == "update":
|
|
91
|
+
await connection.schema_update(self)
|
|
74
92
|
return False
|
|
75
93
|
|
|
76
94
|
def _add(self, column):
|
|
@@ -112,6 +130,9 @@ class SchemaMeta(type):
|
|
|
112
130
|
def create(cls, name):
|
|
113
131
|
return Blueprint(name, cls._connection_name, action="create")
|
|
114
132
|
|
|
133
|
+
def update(cls, name):
|
|
134
|
+
return Blueprint(name, cls._connection_name, action="update")
|
|
135
|
+
|
|
115
136
|
async def drop(cls, name):
|
|
116
137
|
await Database().get_connection(cls._connection_name).schema_drop(name)
|
|
117
138
|
|
|
@@ -262,17 +283,59 @@ class MigrationGenerator:
|
|
|
262
283
|
for field, annotation in annotations.items():
|
|
263
284
|
line = self.column_line(field, annotation)
|
|
264
285
|
if line is not None:
|
|
286
|
+
default_value = vars(model).get(field)
|
|
287
|
+
if default_value is not None:
|
|
288
|
+
if isinstance(default_value, datetime):
|
|
289
|
+
default_value = default_value.isoformat(sep=" ")
|
|
290
|
+
line += f".default({default_value!r})"
|
|
265
291
|
lines.append(f" {line}")
|
|
266
292
|
if use_timestamps:
|
|
267
293
|
lines.append(" table.timestamps()")
|
|
268
294
|
return lines
|
|
269
295
|
|
|
270
|
-
def
|
|
296
|
+
def _schema_call(self, node, table):
|
|
297
|
+
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
|
|
298
|
+
return False
|
|
299
|
+
if node.func.attr not in ("create", "update") or not isinstance(node.func.value, ast.Name) or node.func.value.id != "Schema":
|
|
300
|
+
return False
|
|
301
|
+
return bool(node.args and isinstance(node.args[0], ast.Constant) and node.args[0].value == table)
|
|
302
|
+
|
|
303
|
+
def _previous_blueprint_lines(self, table):
|
|
304
|
+
directory = Path(self.migrations_path)
|
|
305
|
+
if not directory.exists():
|
|
306
|
+
return None
|
|
307
|
+
for path in reversed(sorted(directory.glob("*.py"))):
|
|
308
|
+
try:
|
|
309
|
+
source = path.read_text()
|
|
310
|
+
tree = ast.parse(source)
|
|
311
|
+
except (OSError, SyntaxError):
|
|
312
|
+
continue
|
|
313
|
+
for node in ast.walk(tree):
|
|
314
|
+
if not isinstance(node, ast.AsyncFunctionDef) or node.name != "up":
|
|
315
|
+
continue
|
|
316
|
+
for statement in node.body:
|
|
317
|
+
if not isinstance(statement, ast.AsyncWith):
|
|
318
|
+
continue
|
|
319
|
+
if not statement.items or not self._schema_call(statement.items[0].context_expr, table):
|
|
320
|
+
continue
|
|
321
|
+
lines = []
|
|
322
|
+
for item in statement.body:
|
|
323
|
+
if not isinstance(item, ast.Expr) or not isinstance(item.value, ast.Call):
|
|
324
|
+
continue
|
|
325
|
+
expression = (ast.get_source_segment(source, item) or ast.unparse(item)).strip()
|
|
326
|
+
if expression.startswith("table."):
|
|
327
|
+
lines.append(f" {expression}")
|
|
328
|
+
return lines
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
def render(self, model, previous_lines=None):
|
|
271
332
|
table = self.table_name(model)
|
|
272
|
-
|
|
333
|
+
is_update = previous_lines is not None
|
|
334
|
+
action = "update" if is_update else "create"
|
|
335
|
+
class_name = f"{'Update' if is_update else 'Create'}{inflection.camelize(table)}"
|
|
273
336
|
connection = getattr(model, "__connection__", "default")
|
|
274
337
|
columns = "\n".join(self.blueprint_lines(model))
|
|
275
|
-
|
|
338
|
+
rendered = (
|
|
276
339
|
"from future.migrations import Migration, Schema\n"
|
|
277
340
|
"\n"
|
|
278
341
|
"\n"
|
|
@@ -280,12 +343,14 @@ class MigrationGenerator:
|
|
|
280
343
|
f' __connection__ = "{connection}"\n'
|
|
281
344
|
"\n"
|
|
282
345
|
" async def up(self):\n"
|
|
283
|
-
f' async with Schema.
|
|
346
|
+
f' async with Schema.{action}("{table}") as table:\n'
|
|
284
347
|
f"{columns}\n"
|
|
285
348
|
"\n"
|
|
286
|
-
" async def down(self):\n"
|
|
287
|
-
f' await Schema.drop("{table}")\n'
|
|
288
349
|
)
|
|
350
|
+
if not is_update:
|
|
351
|
+
return rendered + " async def down(self):\n" + f' await Schema.drop("{table}")\n'
|
|
352
|
+
previous = "\n".join(previous_lines)
|
|
353
|
+
return rendered + " async def down(self):\n" + f' async with Schema.update("{table}") as table:\n' + f"{previous}\n"
|
|
289
354
|
|
|
290
355
|
def make(self, model_name=None):
|
|
291
356
|
if model_name is None:
|
|
@@ -301,12 +366,17 @@ class MigrationGenerator:
|
|
|
301
366
|
|
|
302
367
|
def write(self, model, stamp_offset=0):
|
|
303
368
|
table = self.table_name(model)
|
|
304
|
-
|
|
369
|
+
previous_lines = self._previous_blueprint_lines(table)
|
|
370
|
+
action = "update" if previous_lines is not None else "create"
|
|
371
|
+
stamp = datetime.now().strftime("%Y_%m_%d_%H%M%S_%f")
|
|
305
372
|
if stamp_offset:
|
|
306
373
|
stamp = f"{stamp}_{stamp_offset:02d}"
|
|
307
|
-
file_name = f"{stamp}_create_{table}.py"
|
|
308
374
|
directory = Path(self.migrations_path)
|
|
309
375
|
directory.mkdir(parents=True, exist_ok=True)
|
|
310
|
-
path = directory /
|
|
311
|
-
|
|
376
|
+
path = directory / f"{stamp}_{action}_{table}.py"
|
|
377
|
+
collision = 1
|
|
378
|
+
while path.exists():
|
|
379
|
+
path = directory / f"{stamp}_{collision:02d}_{action}_{table}.py"
|
|
380
|
+
collision += 1
|
|
381
|
+
path.write_text(self.render(model, previous_lines=previous_lines))
|
|
312
382
|
return str(path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/Auth0Authentication.py
RENAMED
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/AzureADAuthentication.py
RENAMED
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/BasicAuthentication.py
RENAMED
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/KerberosAuthentication.py
RENAMED
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/KeycloakAuthentication.py
RENAMED
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/OAuth2Authentication.py
RENAMED
|
File without changes
|
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/authentication/SAMLAuthentication.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/HTAccessConfuserMiddleware.py
RENAMED
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/HeaderConfuserMiddleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/SQLiConfuserMiddleware.py
RENAMED
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/ScopeValidationMiddleware.py
RENAMED
|
File without changes
|
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/TestMiddlewareRequest.py
RENAMED
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/TestMiddlewareResponse.py
RENAMED
|
File without changes
|
{future_framework-2.1.1 → future_framework-3.0.1}/future/middleware/WebServerConfuserMiddleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|