future-framework 3.0.0__tar.gz → 3.0.2__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.
Files changed (83) hide show
  1. {future_framework-3.0.0 → future_framework-3.0.2}/PKG-INFO +1 -1
  2. {future_framework-3.0.0 → future_framework-3.0.2}/future/databases/MySQLDatabase.py +21 -4
  3. {future_framework-3.0.0 → future_framework-3.0.2}/future/databases/PostgresDatabase.py +21 -4
  4. {future_framework-3.0.0 → future_framework-3.0.2}/future/databases/SQLiteDatabase.py +21 -4
  5. {future_framework-3.0.0 → future_framework-3.0.2}/future/migrations.py +20 -0
  6. {future_framework-3.0.0 → future_framework-3.0.2}/future/taskscheduler.py +6 -1
  7. {future_framework-3.0.0 → future_framework-3.0.2}/pyproject.toml +1 -1
  8. {future_framework-3.0.0 → future_framework-3.0.2}/LICENSE +0 -0
  9. {future_framework-3.0.0 → future_framework-3.0.2}/README.md +0 -0
  10. {future_framework-3.0.0 → future_framework-3.0.2}/future/__init__.py +0 -0
  11. {future_framework-3.0.0 → future_framework-3.0.2}/future/application.py +0 -0
  12. {future_framework-3.0.0 → future_framework-3.0.2}/future/authentication/Auth0Authentication.py +0 -0
  13. {future_framework-3.0.0 → future_framework-3.0.2}/future/authentication/AzureADAuthentication.py +0 -0
  14. {future_framework-3.0.0 → future_framework-3.0.2}/future/authentication/BasicAuthentication.py +0 -0
  15. {future_framework-3.0.0 → future_framework-3.0.2}/future/authentication/KerberosAuthentication.py +0 -0
  16. {future_framework-3.0.0 → future_framework-3.0.2}/future/authentication/KeycloakAuthentication.py +0 -0
  17. {future_framework-3.0.0 → future_framework-3.0.2}/future/authentication/OAuth2Authentication.py +0 -0
  18. {future_framework-3.0.0 → future_framework-3.0.2}/future/authentication/OpenIdConnectAuthentication.py +0 -0
  19. {future_framework-3.0.0 → future_framework-3.0.2}/future/authentication/SAMLAuthentication.py +0 -0
  20. {future_framework-3.0.0 → future_framework-3.0.2}/future/authentication/__init__.py +0 -0
  21. {future_framework-3.0.0 → future_framework-3.0.2}/future/cli.py +0 -0
  22. {future_framework-3.0.0 → future_framework-3.0.2}/future/controllers/DebugController.py +0 -0
  23. {future_framework-3.0.0 → future_framework-3.0.2}/future/controllers/GraphQLController.py +0 -0
  24. {future_framework-3.0.0 → future_framework-3.0.2}/future/controllers/OpenAPIController.py +0 -0
  25. {future_framework-3.0.0 → future_framework-3.0.2}/future/controllers/WebSocketController.py +0 -0
  26. {future_framework-3.0.0 → future_framework-3.0.2}/future/controllers/WelcomeController.py +0 -0
  27. {future_framework-3.0.0 → future_framework-3.0.2}/future/controllers/__init__.py +0 -0
  28. {future_framework-3.0.0 → future_framework-3.0.2}/future/database.py +0 -0
  29. {future_framework-3.0.0 → future_framework-3.0.2}/future/databases/ClickhouseDatabase.py +0 -0
  30. {future_framework-3.0.0 → future_framework-3.0.2}/future/databases/ElasticsearchDatabase.py +0 -0
  31. {future_framework-3.0.0 → future_framework-3.0.2}/future/databases/MongoDBDatabase.py +0 -0
  32. {future_framework-3.0.0 → future_framework-3.0.2}/future/databases/RedisDatabase.py +0 -0
  33. {future_framework-3.0.0 → future_framework-3.0.2}/future/databases/__init__.py +0 -0
  34. {future_framework-3.0.0 → future_framework-3.0.2}/future/exceptions.py +0 -0
  35. {future_framework-3.0.0 → future_framework-3.0.2}/future/interfaces/IAuthentication.py +0 -0
  36. {future_framework-3.0.0 → future_framework-3.0.2}/future/interfaces/IController.py +0 -0
  37. {future_framework-3.0.0 → future_framework-3.0.2}/future/interfaces/IDatabase.py +0 -0
  38. {future_framework-3.0.0 → future_framework-3.0.2}/future/interfaces/IMiddleware.py +0 -0
  39. {future_framework-3.0.0 → future_framework-3.0.2}/future/interfaces/IModel.py +0 -0
  40. {future_framework-3.0.0 → future_framework-3.0.2}/future/interfaces/IPlugin.py +0 -0
  41. {future_framework-3.0.0 → future_framework-3.0.2}/future/interfaces/ITask.py +0 -0
  42. {future_framework-3.0.0 → future_framework-3.0.2}/future/interfaces/__init__.py +0 -0
  43. {future_framework-3.0.0 → future_framework-3.0.2}/future/interfacing.py +0 -0
  44. {future_framework-3.0.0 → future_framework-3.0.2}/future/lifespan.py +0 -0
  45. {future_framework-3.0.0 → future_framework-3.0.2}/future/logger.py +0 -0
  46. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/BruteforcePreventionMiddleware.py +0 -0
  47. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/CORSMiddleware.py +0 -0
  48. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/CSRFMiddleware.py +0 -0
  49. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/GZipMiddleware.py +0 -0
  50. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/HTAccessConfuserMiddleware.py +0 -0
  51. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/HeaderConfuserMiddleware.py +0 -0
  52. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/RateLimitMiddleware.py +0 -0
  53. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/ResponseCodeConfuserMiddleware.py +0 -0
  54. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/SQLiConfuserMiddleware.py +0 -0
  55. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/ScopeValidationMiddleware.py +0 -0
  56. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/SessionMiddleware.py +0 -0
  57. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/TestMiddlewareRequest.py +0 -0
  58. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/TestMiddlewareResponse.py +0 -0
  59. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/WebServerConfuserMiddleware.py +0 -0
  60. {future_framework-3.0.0 → future_framework-3.0.2}/future/middleware/__init__.py +0 -0
  61. {future_framework-3.0.0 → future_framework-3.0.2}/future/models/PostModel.py +0 -0
  62. {future_framework-3.0.0 → future_framework-3.0.2}/future/models/Query.py +0 -0
  63. {future_framework-3.0.0 → future_framework-3.0.2}/future/models/UserModel.py +0 -0
  64. {future_framework-3.0.0 → future_framework-3.0.2}/future/models/__init__.py +0 -0
  65. {future_framework-3.0.0 → future_framework-3.0.2}/future/openapi.py +0 -0
  66. {future_framework-3.0.0 → future_framework-3.0.2}/future/plugins/ElasticsearchPlugin.py +0 -0
  67. {future_framework-3.0.0 → future_framework-3.0.2}/future/plugins/__init__.py +0 -0
  68. {future_framework-3.0.0 → future_framework-3.0.2}/future/request.py +0 -0
  69. {future_framework-3.0.0 → future_framework-3.0.2}/future/response.py +0 -0
  70. {future_framework-3.0.0 → future_framework-3.0.2}/future/routing.py +0 -0
  71. {future_framework-3.0.0 → future_framework-3.0.2}/future/seeder.py +0 -0
  72. {future_framework-3.0.0 → future_framework-3.0.2}/future/settings.py +0 -0
  73. {future_framework-3.0.0 → future_framework-3.0.2}/future/tasks/CheckDNSTask.py +0 -0
  74. {future_framework-3.0.0 → future_framework-3.0.2}/future/tasks/CheckDiskUsageTask.py +0 -0
  75. {future_framework-3.0.0 → future_framework-3.0.2}/future/tasks/CheckHttpStatusTask.py +0 -0
  76. {future_framework-3.0.0 → future_framework-3.0.2}/future/tasks/CheckMemoryUsageTask.py +0 -0
  77. {future_framework-3.0.0 → future_framework-3.0.2}/future/tasks/CheckSSHBannerTask.py +0 -0
  78. {future_framework-3.0.0 → future_framework-3.0.2}/future/tasks/CheckSystemUptimeTask.py +0 -0
  79. {future_framework-3.0.0 → future_framework-3.0.2}/future/tasks/DailyBackupTask.py +0 -0
  80. {future_framework-3.0.0 → future_framework-3.0.2}/future/tasks/SyncTaskExample.py +0 -0
  81. {future_framework-3.0.0 → future_framework-3.0.2}/future/tasks/__init__.py +0 -0
  82. {future_framework-3.0.0 → future_framework-3.0.2}/future/testclient.py +0 -0
  83. {future_framework-3.0.0 → future_framework-3.0.2}/future/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: future-framework
3
- Version: 3.0.0
3
+ Version: 3.0.2
4
4
  Summary: ASGI-based framework for web APIs
5
5
  License: LICENSE
6
6
  License-File: LICENSE
@@ -172,7 +172,6 @@ class MySQLDatabase(IDatabase):
172
172
  async with self.client.connect() as connection:
173
173
  rows = (await connection.execute(sql, {"table": blueprint.name})).mappings().all()
174
174
  existing = {row["COLUMN_NAME"] for row in rows}
175
- common = [column.name for column in blueprint.columns if column.name in existing]
176
175
  temporary = f"_future_migrate_{blueprint.name}"
177
176
  backup = f"_future_backup_{blueprint.name}"
178
177
  parts = self._schema_parts(blueprint)
@@ -180,9 +179,27 @@ class MySQLDatabase(IDatabase):
180
179
  await connection.execute(text(f"DROP TABLE IF EXISTS `{temporary}`"))
181
180
  await connection.execute(text(f"DROP TABLE IF EXISTS `{backup}`"))
182
181
  await connection.execute(text(f"CREATE TABLE `{temporary}` ({', '.join(parts)})"))
183
- if common:
184
- columns = ", ".join(f"`{name}`" for name in common)
185
- await connection.execute(text(f"INSERT INTO `{temporary}` ({columns}) SELECT {columns} FROM `{blueprint.name}`"))
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)
186
203
  await connection.execute(text(f"RENAME TABLE `{blueprint.name}` TO `{backup}`, `{temporary}` TO `{blueprint.name}`"))
187
204
  await connection.execute(text(f"DROP TABLE `{backup}`"))
188
205
  return {"status": "updated", "table": blueprint.name, "columns": sorted(column.name for column in blueprint.columns)}
@@ -180,15 +180,32 @@ class PostgresDatabase(IDatabase):
180
180
  async with self.client.connect() as connection:
181
181
  rows = (await connection.execute(sql, {"table": blueprint.name})).mappings().all()
182
182
  existing = {row["column_name"] for row in rows}
183
- common = [column.name for column in blueprint.columns if column.name in existing]
184
183
  temporary = f"_future_{uuid4().hex[:8]}_{blueprint.name}"[:63]
185
184
  parts, indexes = self._schema_parts(blueprint)
186
185
  async with self.client.begin() as connection:
187
186
  await connection.execute(text(f'DROP TABLE IF EXISTS "{temporary}"'))
188
187
  await connection.execute(text(f'CREATE TABLE "{temporary}" ({", ".join(parts)})'))
189
- if common:
190
- columns = ", ".join(f'"{name}"' for name in common)
191
- await connection.execute(text(f'INSERT INTO "{temporary}" ({columns}) SELECT {columns} FROM "{blueprint.name}"'))
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)
192
209
  await connection.execute(text(f'DROP TABLE "{blueprint.name}"'))
193
210
  await connection.execute(text(f'ALTER TABLE "{temporary}" RENAME TO "{blueprint.name}"'))
194
211
  for index_name in indexes:
@@ -178,16 +178,33 @@ class SQLiteDatabase(IDatabase):
178
178
  rows = (await connection.execute(text(f'PRAGMA table_info("{blueprint.name}")'))).mappings().all()
179
179
  existing = {row["name"] for row in rows}
180
180
  desired = {column.name for column in blueprint.columns}
181
- common = [column.name for column in blueprint.columns if column.name in existing]
182
181
  temporary = f"_future_migrate_{blueprint.name}"
183
182
  definitions, _ = self._schema_parts(blueprint)
184
183
  _, indexes = self._schema_parts(blueprint, index_table=blueprint.name)
185
184
  async with self.client.begin() as connection:
186
185
  await connection.execute(text(f'DROP TABLE IF EXISTS "{temporary}"'))
187
186
  await connection.execute(text(f'CREATE TABLE "{temporary}" ({", ".join(definitions)})'))
188
- if common:
189
- columns = ", ".join(f'"{name}"' for name in common)
190
- await connection.execute(text(f'INSERT INTO "{temporary}" ({columns}) SELECT {columns} FROM "{blueprint.name}"'))
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)
191
208
  await connection.execute(text(f'DROP TABLE "{blueprint.name}"'))
192
209
  await connection.execute(text(f'ALTER TABLE "{temporary}" RENAME TO "{blueprint.name}"'))
193
210
  for index_sql in indexes:
@@ -55,6 +55,21 @@ class Column:
55
55
  self.default_value = value
56
56
  return self
57
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
+
58
73
 
59
74
  class Blueprint:
60
75
  def __init__(self, name, connection_name, action="create"):
@@ -268,6 +283,11 @@ class MigrationGenerator:
268
283
  for field, annotation in annotations.items():
269
284
  line = self.column_line(field, annotation)
270
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})"
271
291
  lines.append(f" {line}")
272
292
  if use_timestamps:
273
293
  lines.append(" table.timestamps()")
@@ -27,6 +27,7 @@ class ScheduledTask:
27
27
  self.jitter = task.jitter
28
28
  self.last_run: Optional[datetime] = None
29
29
  self.next_run: Optional[datetime] = None
30
+ self.running = False
30
31
 
31
32
  if self.start_time is None:
32
33
  self.start_time = datetime.now()
@@ -85,6 +86,7 @@ class CronScheduler:
85
86
 
86
87
  async def _run_task(self, scheduled: ScheduledTask) -> None:
87
88
  """Run a single task."""
89
+ scheduled.running = True
88
90
  try:
89
91
  log.debug(f"Running scheduled task '{scheduled.name}'")
90
92
  await scheduled.task.run()
@@ -99,6 +101,8 @@ class CronScheduler:
99
101
  except Exception as e:
100
102
  log.error(f"Error running scheduled task '{scheduled.name}': {e}")
101
103
  # Don't update last_run on error, so it will retry next cycle
104
+ finally:
105
+ scheduled.running = False
102
106
 
103
107
  async def _scheduler_loop(self) -> None:
104
108
  """Main scheduler loop that checks for tasks to run."""
@@ -110,7 +114,8 @@ class CronScheduler:
110
114
 
111
115
  # Check which tasks need to run
112
116
  for scheduled in self.tasks.values():
113
- if scheduled.next_run and now >= scheduled.next_run:
117
+ if scheduled.next_run and now >= scheduled.next_run and not scheduled.running:
118
+ scheduled.running = True
114
119
  tasks_to_run.append(scheduled)
115
120
 
116
121
  # Run tasks that are due
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "future-framework"
3
- version = "3.0.0"
3
+ version = "3.0.2"
4
4
  description = "ASGI-based framework for web APIs"
5
5
  authors = ["nicolaipre"]
6
6
  license = "LICENSE"