future-framework 2.0.0__tar.gz → 2.1.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.
Files changed (83) hide show
  1. {future_framework-2.0.0 → future_framework-2.1.0}/PKG-INFO +2 -1
  2. {future_framework-2.0.0 → future_framework-2.1.0}/future/databases/ElasticsearchDatabase.py +18 -3
  3. {future_framework-2.0.0 → future_framework-2.1.0}/future/databases/PostgresDatabase.py +16 -8
  4. {future_framework-2.0.0 → future_framework-2.1.0}/future/response.py +18 -62
  5. {future_framework-2.0.0 → future_framework-2.1.0}/future/seeder.py +52 -0
  6. {future_framework-2.0.0 → future_framework-2.1.0}/pyproject.toml +1 -1
  7. {future_framework-2.0.0 → future_framework-2.1.0}/LICENSE +0 -0
  8. {future_framework-2.0.0 → future_framework-2.1.0}/README.md +0 -0
  9. {future_framework-2.0.0 → future_framework-2.1.0}/future/__init__.py +0 -0
  10. {future_framework-2.0.0 → future_framework-2.1.0}/future/application.py +0 -0
  11. {future_framework-2.0.0 → future_framework-2.1.0}/future/authentication/Auth0Authentication.py +0 -0
  12. {future_framework-2.0.0 → future_framework-2.1.0}/future/authentication/AzureADAuthentication.py +0 -0
  13. {future_framework-2.0.0 → future_framework-2.1.0}/future/authentication/BasicAuthentication.py +0 -0
  14. {future_framework-2.0.0 → future_framework-2.1.0}/future/authentication/KerberosAuthentication.py +0 -0
  15. {future_framework-2.0.0 → future_framework-2.1.0}/future/authentication/KeycloakAuthentication.py +0 -0
  16. {future_framework-2.0.0 → future_framework-2.1.0}/future/authentication/OAuth2Authentication.py +0 -0
  17. {future_framework-2.0.0 → future_framework-2.1.0}/future/authentication/OpenIdConnectAuthentication.py +0 -0
  18. {future_framework-2.0.0 → future_framework-2.1.0}/future/authentication/SAMLAuthentication.py +0 -0
  19. {future_framework-2.0.0 → future_framework-2.1.0}/future/authentication/__init__.py +0 -0
  20. {future_framework-2.0.0 → future_framework-2.1.0}/future/cli.py +0 -0
  21. {future_framework-2.0.0 → future_framework-2.1.0}/future/controllers/DebugController.py +0 -0
  22. {future_framework-2.0.0 → future_framework-2.1.0}/future/controllers/GraphQLController.py +0 -0
  23. {future_framework-2.0.0 → future_framework-2.1.0}/future/controllers/OpenAPIController.py +0 -0
  24. {future_framework-2.0.0 → future_framework-2.1.0}/future/controllers/WebSocketController.py +0 -0
  25. {future_framework-2.0.0 → future_framework-2.1.0}/future/controllers/WelcomeController.py +0 -0
  26. {future_framework-2.0.0 → future_framework-2.1.0}/future/controllers/__init__.py +0 -0
  27. {future_framework-2.0.0 → future_framework-2.1.0}/future/database.py +0 -0
  28. {future_framework-2.0.0 → future_framework-2.1.0}/future/databases/ClickhouseDatabase.py +0 -0
  29. {future_framework-2.0.0 → future_framework-2.1.0}/future/databases/MongoDBDatabase.py +0 -0
  30. {future_framework-2.0.0 → future_framework-2.1.0}/future/databases/MySQLDatabase.py +0 -0
  31. {future_framework-2.0.0 → future_framework-2.1.0}/future/databases/RedisDatabase.py +0 -0
  32. {future_framework-2.0.0 → future_framework-2.1.0}/future/databases/SQLiteDatabase.py +0 -0
  33. {future_framework-2.0.0 → future_framework-2.1.0}/future/databases/__init__.py +0 -0
  34. {future_framework-2.0.0 → future_framework-2.1.0}/future/exceptions.py +0 -0
  35. {future_framework-2.0.0 → future_framework-2.1.0}/future/interfaces/IAuthentication.py +0 -0
  36. {future_framework-2.0.0 → future_framework-2.1.0}/future/interfaces/IController.py +0 -0
  37. {future_framework-2.0.0 → future_framework-2.1.0}/future/interfaces/IDatabase.py +0 -0
  38. {future_framework-2.0.0 → future_framework-2.1.0}/future/interfaces/IMiddleware.py +0 -0
  39. {future_framework-2.0.0 → future_framework-2.1.0}/future/interfaces/IModel.py +0 -0
  40. {future_framework-2.0.0 → future_framework-2.1.0}/future/interfaces/IPlugin.py +0 -0
  41. {future_framework-2.0.0 → future_framework-2.1.0}/future/interfaces/ITask.py +0 -0
  42. {future_framework-2.0.0 → future_framework-2.1.0}/future/interfaces/__init__.py +0 -0
  43. {future_framework-2.0.0 → future_framework-2.1.0}/future/interfacing.py +0 -0
  44. {future_framework-2.0.0 → future_framework-2.1.0}/future/lifespan.py +0 -0
  45. {future_framework-2.0.0 → future_framework-2.1.0}/future/logger.py +0 -0
  46. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/BruteforcePreventionMiddleware.py +0 -0
  47. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/CORSMiddleware.py +0 -0
  48. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/CSRFMiddleware.py +0 -0
  49. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/GZipMiddleware.py +0 -0
  50. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/HTAccessConfuserMiddleware.py +0 -0
  51. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/HeaderConfuserMiddleware.py +0 -0
  52. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/RateLimitMiddleware.py +0 -0
  53. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/ResponseCodeConfuserMiddleware.py +0 -0
  54. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/SQLiConfuserMiddleware.py +0 -0
  55. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/ScopeValidationMiddleware.py +0 -0
  56. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/SessionMiddleware.py +0 -0
  57. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/TestMiddlewareRequest.py +0 -0
  58. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/TestMiddlewareResponse.py +0 -0
  59. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/WebServerConfuserMiddleware.py +0 -0
  60. {future_framework-2.0.0 → future_framework-2.1.0}/future/middleware/__init__.py +0 -0
  61. {future_framework-2.0.0 → future_framework-2.1.0}/future/migrations.py +0 -0
  62. {future_framework-2.0.0 → future_framework-2.1.0}/future/models/PostModel.py +0 -0
  63. {future_framework-2.0.0 → future_framework-2.1.0}/future/models/Query.py +0 -0
  64. {future_framework-2.0.0 → future_framework-2.1.0}/future/models/UserModel.py +0 -0
  65. {future_framework-2.0.0 → future_framework-2.1.0}/future/models/__init__.py +0 -0
  66. {future_framework-2.0.0 → future_framework-2.1.0}/future/openapi.py +0 -0
  67. {future_framework-2.0.0 → future_framework-2.1.0}/future/plugins/ElasticsearchPlugin.py +0 -0
  68. {future_framework-2.0.0 → future_framework-2.1.0}/future/plugins/__init__.py +0 -0
  69. {future_framework-2.0.0 → future_framework-2.1.0}/future/request.py +0 -0
  70. {future_framework-2.0.0 → future_framework-2.1.0}/future/routing.py +0 -0
  71. {future_framework-2.0.0 → future_framework-2.1.0}/future/settings.py +0 -0
  72. {future_framework-2.0.0 → future_framework-2.1.0}/future/tasks/CheckDNSTask.py +0 -0
  73. {future_framework-2.0.0 → future_framework-2.1.0}/future/tasks/CheckDiskUsageTask.py +0 -0
  74. {future_framework-2.0.0 → future_framework-2.1.0}/future/tasks/CheckHttpStatusTask.py +0 -0
  75. {future_framework-2.0.0 → future_framework-2.1.0}/future/tasks/CheckMemoryUsageTask.py +0 -0
  76. {future_framework-2.0.0 → future_framework-2.1.0}/future/tasks/CheckSSHBannerTask.py +0 -0
  77. {future_framework-2.0.0 → future_framework-2.1.0}/future/tasks/CheckSystemUptimeTask.py +0 -0
  78. {future_framework-2.0.0 → future_framework-2.1.0}/future/tasks/DailyBackupTask.py +0 -0
  79. {future_framework-2.0.0 → future_framework-2.1.0}/future/tasks/SyncTaskExample.py +0 -0
  80. {future_framework-2.0.0 → future_framework-2.1.0}/future/tasks/__init__.py +0 -0
  81. {future_framework-2.0.0 → future_framework-2.1.0}/future/taskscheduler.py +0 -0
  82. {future_framework-2.0.0 → future_framework-2.1.0}/future/testclient.py +0 -0
  83. {future_framework-2.0.0 → future_framework-2.1.0}/future/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: future-framework
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: ASGI-based framework for web APIs
5
5
  License: LICENSE
6
6
  License-File: LICENSE
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Programming Language :: Python :: 3.15
14
15
  Requires-Dist: aiomysql (>=0.2.0,<0.3.0)
15
16
  Requires-Dist: aiosqlite (>=0.21.0,<0.22.0)
16
17
  Requires-Dist: asynch (>=0.2.5,<0.3.0)
@@ -42,7 +42,10 @@ class ElasticsearchDatabase(IDatabase):
42
42
  return None
43
43
  if not result or "_source" not in result:
44
44
  return None
45
- return model.__class__(**result["_source"])
45
+ source = dict(result["_source"] or {})
46
+ if result.get("_id") is not None and not source.get("id"):
47
+ source["id"] = result["_id"]
48
+ return model.__class__(**source)
46
49
 
47
50
  async def all(self, model):
48
51
  if self.client is None:
@@ -51,7 +54,13 @@ class ElasticsearchDatabase(IDatabase):
51
54
  result = await self.client.search(index=model.tableize(), body={"query": {"match_all": {}}, "size": 10000})
52
55
  except Exception:
53
56
  return []
54
- return [model.__class__(**hit["_source"]) for hit in result.get("hits", {}).get("hits", [])]
57
+ rows = []
58
+ for hit in result.get("hits", {}).get("hits", []):
59
+ source = dict(hit.get("_source") or {})
60
+ if hit.get("_id") is not None and not source.get("id"):
61
+ source["id"] = hit["_id"]
62
+ rows.append(model.__class__(**source))
63
+ return rows
55
64
 
56
65
  async def get(self, model, wheres, limit=None, orders=None):
57
66
  if self.client is None:
@@ -85,7 +94,13 @@ class ElasticsearchDatabase(IDatabase):
85
94
  result = await self.client.search(index=model.tableize(), body=body)
86
95
  except Exception:
87
96
  return []
88
- return [model.__class__(**hit["_source"]) for hit in result.get("hits", {}).get("hits", [])]
97
+ rows = []
98
+ for hit in result.get("hits", {}).get("hits", []):
99
+ source = dict(hit.get("_source") or {})
100
+ if hit.get("_id") is not None and not source.get("id"):
101
+ source["id"] = hit["_id"]
102
+ rows.append(model.__class__(**source))
103
+ return rows
89
104
 
90
105
  async def delete(self, model):
91
106
  if self.client is None:
@@ -1,5 +1,6 @@
1
1
  from future.interfaces.IDatabase import IDatabase
2
- from sqlalchemy import text
2
+ from sqlalchemy import BigInteger, String, bindparam, text
3
+ from sqlalchemy.dialects.postgresql import ARRAY
3
4
  from sqlalchemy.ext.asyncio import create_async_engine
4
5
  from urllib.parse import quote_plus
5
6
 
@@ -61,17 +62,21 @@ class PostgresDatabase(IDatabase):
61
62
  table = model.tableize()
62
63
  clauses = []
63
64
  params = {}
65
+ array_binds = {}
64
66
  for index, (column, operator, value) in enumerate(wheres):
65
67
  if operator == "in":
66
68
  values = list(value)
67
69
  if not values:
68
70
  return []
69
- keys = []
70
- for item_index, item in enumerate(values):
71
- key = f"v{index}_{item_index}"
72
- keys.append(f":{key}")
73
- params[key] = item
74
- clauses.append(f'"{column}" IN ({", ".join(keys)})')
71
+ # One array bind avoids Postgres' ~32767 bind-variable limit.
72
+ key = f"v{index}"
73
+ clauses.append(f'"{column}" = ANY(:{key})')
74
+ params[key] = values
75
+ sample = values[0]
76
+ if isinstance(sample, int) and not isinstance(sample, bool):
77
+ array_binds[key] = ARRAY(BigInteger)
78
+ else:
79
+ array_binds[key] = ARRAY(String)
75
80
  continue
76
81
  if operator == "like":
77
82
  key = f"v{index}"
@@ -90,8 +95,11 @@ class PostgresDatabase(IDatabase):
90
95
  sql += " ORDER BY " + ", ".join(f'"{column}" {direction}' for column, direction in orders)
91
96
  if limit is not None:
92
97
  sql += f" LIMIT {int(limit)}"
98
+ stmt = text(sql)
99
+ for key, array_type in array_binds.items():
100
+ stmt = stmt.bindparams(bindparam(key, type_=array_type))
93
101
  async with self.client.connect() as connection:
94
- rows = (await connection.execute(text(sql), params)).mappings().all()
102
+ rows = (await connection.execute(stmt, params)).mappings().all()
95
103
  return [model.__class__(**dict(row)) for row in rows]
96
104
 
97
105
  async def delete(self, model):
@@ -30,7 +30,7 @@ class Response:
30
30
  header_map["content-type"] = content_type
31
31
  # Preserve Set-Cookie headers already appended via set_cookie
32
32
  cookies = [pair for pair in self.headers if pair[0].lower() == b"set-cookie"]
33
- self.headers = [[key.encode(), value.encode()] for key, value in header_map.items()]
33
+ self.headers = [[key.lower().encode(), value.encode()] for key, value in header_map.items()]
34
34
  self.headers.extend(cookies)
35
35
  return self
36
36
 
@@ -54,11 +54,6 @@ class Response:
54
54
  def file(self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None, content_type: str = "application/octet-stream") -> "Response":
55
55
  return self._set(body=body, status=status, headers=headers, content_type=content_type)
56
56
 
57
- def stream(self, chunks, status: int = 200, headers: dict[str, str] | None = None, content_type: str = "application/octet-stream") -> "StreamingResponse":
58
- streamed = StreamingResponse(chunks, status=status, headers=headers, content_type=content_type)
59
- streamed.headers.extend([pair for pair in self.headers if pair[0].lower() == b"set-cookie"])
60
- return streamed
61
-
62
57
  def image(self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None, content_type: str = "image/png", file_path: str | None = None) -> "Response":
63
58
  if file_path:
64
59
  with open(file_path, "rb") as f:
@@ -149,72 +144,33 @@ class WebSocketResponse:
149
144
  await self.send_text(f"Echo: {text}")
150
145
 
151
146
 
152
- # Temporary wrappers for migration
153
- class PlainTextResponse(Response):
154
- def __init__(self, body: str = "", status: int = 200, headers: dict[str, str] | None = None) -> None:
155
- super().__init__()
156
- self.text(body, status=status, headers=headers)
157
-
158
-
159
- class JSONResponse(Response):
160
- def __init__(self, data: Any = None, status: int = 200, headers: dict[str, str] | None = None) -> None:
161
- super().__init__()
162
- self.json(data, status=status, headers=headers)
163
-
164
-
165
- class EmptyResponse(Response):
166
- def __init__(self, status: int = 204, headers: dict[str, str] | None = None) -> None:
167
- super().__init__()
168
- self.empty(status=status, headers=headers)
169
-
170
-
171
- class HTMLResponse(Response):
172
- def __init__(self, html: str = "", status: int = 200, headers: dict[str, str] | None = None) -> None:
173
- super().__init__()
174
- self.html(html, status=status, headers=headers)
175
-
176
-
177
- class PNGResponse(Response):
178
- def __init__(self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None) -> None:
179
- super().__init__()
180
- self.image(body, status=status, headers=headers, content_type="image/png")
181
-
182
-
183
- class ImageResponse(Response):
184
- def __init__(
185
- self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None, image_content_type: str = "image/png", file_path: str | None = None
186
- ) -> None:
187
- super().__init__()
188
- self.image(body, status=status, headers=headers, content_type=image_content_type, file_path=file_path)
189
-
190
-
191
- class RedirectResponse(Response):
192
- def __init__(self, url: str = "", status: int = 302, headers: dict[str, str] | None = None) -> None:
193
- super().__init__()
194
- if url:
195
- self.redirect(url, status=status, headers=headers)
196
-
197
-
198
- class FileResponse(Response):
199
- def __init__(self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None) -> None:
200
- super().__init__()
201
- self.file(body, status=status, headers=headers)
202
-
203
-
204
147
  class StreamingResponse(Response):
205
148
  def __init__(self, body=(), status: int = 200, headers: dict[str, str] | None = None, content_type: Optional[str] = None) -> None:
206
149
  super().__init__(body=b"", status=status, headers=headers, content_type=content_type or "application/octet-stream")
207
150
  self._chunks = body
208
151
 
209
152
  async def __call__(self, send: Any) -> None:
210
- await send({"type": "http.response.start", "status": self.status, "headers": self.headers})
153
+ await send({"type": "http.response.start", "status": int(self.status), "headers": self.headers})
211
154
  chunks = self._chunks
212
- if isinstance(chunks, (bytes, str)):
155
+ if isinstance(chunks, (bytes, bytearray, memoryview, str)):
213
156
  chunks = [chunks]
214
157
  if hasattr(chunks, "__aiter__"):
215
158
  async for chunk in chunks:
216
- await send({"type": "http.response.body", "body": chunk if isinstance(chunk, bytes) else str(chunk).encode("utf-8"), "more_body": True})
159
+ if isinstance(chunk, (bytes, bytearray, memoryview)):
160
+ body = bytes(chunk)
161
+ elif isinstance(chunk, str):
162
+ body = chunk.encode("utf-8")
163
+ else:
164
+ raise TypeError(f"stream chunk must be bytes or str, got {type(chunk).__name__}")
165
+ await send({"type": "http.response.body", "body": body, "more_body": True})
217
166
  else:
218
167
  for chunk in chunks:
219
- await send({"type": "http.response.body", "body": chunk if isinstance(chunk, bytes) else str(chunk).encode("utf-8"), "more_body": True})
168
+ if isinstance(chunk, (bytes, bytearray, memoryview)):
169
+ body = bytes(chunk)
170
+ elif isinstance(chunk, str):
171
+ body = chunk.encode("utf-8")
172
+ else:
173
+ raise TypeError(f"stream chunk must be bytes or str, got {type(chunk).__name__}")
174
+ await send({"type": "http.response.body", "body": body, "more_body": True})
220
175
  await send({"type": "http.response.body", "body": b"", "more_body": False})
176
+
@@ -73,10 +73,30 @@ class SeedGenerator:
73
73
  type_name = getattr(annotation, "__name__", str(annotation))
74
74
  if field == "id":
75
75
  return "fake.uuid4()"
76
+ if field in ("timestamp", "published_at", "generated_at", "joined_at", "trades_synced_at", "last_login_at"):
77
+ return 'fake.date_time_between(start_date="-1y", end_date="now")'
76
78
  if type_name == "datetime":
77
79
  return "fake.date_time_this_year()"
80
+ if field == "historical_trades_offset":
81
+ return "0"
82
+ if field in ("follower_count", "following_count"):
83
+ return "fake.random_int(0, 5000)"
84
+ if field == "average_hold_time":
85
+ return "fake.random_int(1, 365)"
86
+ if field == "rating":
87
+ return "fake.random_int(0, 5)"
88
+ if field == "window_hours":
89
+ return "fake.random_int(1, 168)"
90
+ if field == "unique_traders":
91
+ return "fake.random_int(2, 20)"
78
92
  if type_name == "int":
79
93
  return "fake.random_int(1, 9999)"
94
+ if field == "price":
95
+ return "float(fake.pyfloat(min_value=5, max_value=1500, right_digits=2))"
96
+ if field == "percentage_change":
97
+ return "float(fake.pyfloat(min_value=-25, max_value=25, right_digits=2))"
98
+ if field == "strength":
99
+ return "float(fake.pyfloat(min_value=0, max_value=100, right_digits=2))"
80
100
  if type_name == "float":
81
101
  return "float(fake.pyfloat(min_value=1, max_value=100))"
82
102
  if type_name == "bool":
@@ -85,6 +105,38 @@ class SeedGenerator:
85
105
  return "str(fake.random_int(1000, 9999))"
86
106
  if "email" in field:
87
107
  return "fake.email()"
108
+ if field == "firstname":
109
+ return "fake.first_name()"
110
+ if field == "lastname":
111
+ return "fake.last_name()"
112
+ if field == "instrument_name":
113
+ return "fake.company()"
114
+ if field == "symbol":
115
+ return 'fake.lexify(text="????").upper()'
116
+ if field in ("logo_url", "image_url", "avatar_uri"):
117
+ return "fake.image_url()"
118
+ if field == "url":
119
+ return "fake.url()"
120
+ if field == "currency":
121
+ return 'fake.random_element(elements=("NOK", "SEK", "DKK", "EUR", "USD"))'
122
+ if field == "language":
123
+ return 'fake.random_element(elements=("nb", "sv", "da", "en"))'
124
+ if field == "trade_type" or field == "side":
125
+ return 'fake.random_element(elements=("BUY", "SELL"))'
126
+ if field == "percentage_change_interval":
127
+ return 'fake.random_element(elements=("TODAY", "ONE_WEEK", "ONE_MONTH", "THREE_MONTHS", "ONE_YEAR"))'
128
+ if field == "group_type":
129
+ return 'fake.random_element(elements=("COMMON_SHARE", "FUND", "ETF", "WARRANT"))'
130
+ if field == "kind":
131
+ return 'fake.random_element(elements=("instruments", "users"))'
132
+ if field == "members":
133
+ return "fake.json()"
134
+ if field == "title":
135
+ return "fake.sentence(nb_words=8)"
136
+ if field in ("summary", "reason"):
137
+ return "fake.sentence()"
138
+ if field == "password_hash":
139
+ return "fake.sha256()"
88
140
  if "name" in field or "username" in field:
89
141
  return "fake.user_name()"
90
142
  if field in ("description", "text", "body", "content") or field.endswith("_original") or field.endswith("_translated"):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "future-framework"
3
- version = "2.0.0"
3
+ version = "2.1.0"
4
4
  description = "ASGI-based framework for web APIs"
5
5
  authors = ["nicolaipre"]
6
6
  license = "LICENSE"