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.
Files changed (64) hide show
  1. future/__init__.py +3 -0
  2. future/application.py +656 -0
  3. future/authentication/Auth0Authentication.py +9 -0
  4. future/authentication/Authentication.py +11 -0
  5. future/authentication/AzureADAuthentication.py +9 -0
  6. future/authentication/BasicAuthentication.py +9 -0
  7. future/authentication/KerberosAuthentication.py +9 -0
  8. future/authentication/KeycloakAuthentication.py +9 -0
  9. future/authentication/OAuth2Authentication.py +9 -0
  10. future/authentication/OpenIdConnectAuthentication.py +9 -0
  11. future/authentication/SAMLAuthentication.py +9 -0
  12. future/cli/__init__.py +1 -0
  13. future/cli/main.py +608 -0
  14. future/cli/stubs.py +114 -0
  15. future/controllers/__init__.py +4 -0
  16. future/controllers/base.py +8 -0
  17. future/controllers/builtins.py +41 -0
  18. future/controllers/graphql.py +23 -0
  19. future/controllers/openapi.py +224 -0
  20. future/databases/Clickhouse.py +176 -0
  21. future/databases/Connections.py +20 -0
  22. future/databases/Database.py +66 -0
  23. future/databases/Elasticsearch.py +146 -0
  24. future/databases/MongoDB.py +177 -0
  25. future/databases/MySQL.py +215 -0
  26. future/databases/Postgres.py +214 -0
  27. future/databases/Redis.py +146 -0
  28. future/databases/SQLite.py +219 -0
  29. future/exceptions.py +27 -0
  30. future/graphql/__init__.py +1 -0
  31. future/graphql/schema.py +148 -0
  32. future/lifespan.py +70 -0
  33. future/logger.py +53 -0
  34. future/middleware/Middleware.py +201 -0
  35. future/middleware/SessionMiddleware.py +77 -0
  36. future/middleware/__init__.py +21 -0
  37. future/migrations/Blueprint.py +54 -0
  38. future/migrations/Column.py +33 -0
  39. future/migrations/Migration.py +11 -0
  40. future/migrations/MigrationGenerator.py +137 -0
  41. future/migrations/Migrator.py +72 -0
  42. future/migrations/Schema.py +21 -0
  43. future/models/__init__.py +1 -0
  44. future/models/model.py +224 -0
  45. future/openapi.py +233 -0
  46. future/plugins/ElasticsearchPlugin.py +496 -0
  47. future/plugins/__init__.py +9 -0
  48. future/request.py +143 -0
  49. future/response.py +201 -0
  50. future/routing.py +342 -0
  51. future/seeds/SeedGenerator.py +95 -0
  52. future/seeds/SeedRunner.py +42 -0
  53. future/seeds/Seeder.py +9 -0
  54. future/settings.py +94 -0
  55. future/tasks/__init__.py +14 -0
  56. future/tasks/scheduler.py +267 -0
  57. future/testing/__init__.py +1 -0
  58. future/testing/client.py +135 -0
  59. future/types.py +47 -0
  60. future_framework-1.1.0.dist-info/METADATA +68 -0
  61. future_framework-1.1.0.dist-info/RECORD +64 -0
  62. future_framework-1.1.0.dist-info/WHEEL +4 -0
  63. future_framework-1.1.0.dist-info/entry_points.txt +3 -0
  64. future_framework-1.1.0.dist-info/licenses/LICENSE +21 -0
future/settings.py ADDED
@@ -0,0 +1,94 @@
1
+ from os import environ as env
2
+
3
+ from dotenv import load_dotenv
4
+
5
+
6
+ load_dotenv(dotenv_path=".env") # FIXME: dotenv not used correctly. Shouldn't need both os.environ and dotenv...
7
+ # os.environ["APP_ENV"] = "dev"
8
+ # port = int(os.environ.get("APP_PORT", 44777))
9
+
10
+
11
+ # Application settings sourced from .env
12
+ APP_NAME = str(env.get("APP_NAME", "Future"))
13
+ APP_VERSION = str(env.get("APP_VERSION", "1.0"))
14
+ APP_DESCRIPTION = str(env.get("APP_DESCRIPTION", "A short description"))
15
+ APP_DEBUG = env.get("APP_DEBUG", "False").lower() == "true"
16
+ APP_LOG_LEVEL = str("DEBUG" if APP_DEBUG else env.get("APP_LOG_LEVEL", "INFO"))
17
+ APP_ACCESS_LOG = env.get("APP_ACCESS_LOG", "False").lower() == "true"
18
+ APP_WORKERS = int(env.get("APP_WORKERS", 4))
19
+ APP_HOST = str(env.get("APP_HOST", "127.0.0.1"))
20
+ APP_PORT = int(env.get("APP_PORT", 9000))
21
+ APP_SSO = env.get("APP_SSO", "False").lower() == "true"
22
+ APP_KEY = str(env.get("APP_KEY", "secret")) # os.urandom(24) # TODO: add to cli --generate
23
+ APP_REGISTRATION = env.get("APP_REGISTRATION", "False").lower() == "true"
24
+ APP_SSL_CERT_FILE = str(env.get("APP_SSL_CERT_FILE", "./cert.pem"))
25
+ APP_SSL_KEY_FILE = str(env.get("APP_SSL_KEY_FILE", "./key.pem"))
26
+ APP_SSL_PASSPHRASE = str(env.get("APP_SSL_PASSPHRASE", "changeme"))
27
+ APP_DOMAIN = str(env.get("APP_DOMAIN", "example.com"))
28
+
29
+ # Database settings sourced from .env
30
+ DB_DRIVER = str(env.get("DB_DRIVER", "sqlite"))
31
+ DB_HOST = str(env.get("DB_HOST", "127.0.0.1"))
32
+ DB_PORT = int(env.get("DB_PORT", 3306))
33
+ DB_DATABASE = str(env.get("DB_DATABASE", "database"))
34
+ DB_USERNAME = str(env.get("DB_USERNAME", ""))
35
+ DB_PASSWORD = str(env.get("DB_PASSWORD", ""))
36
+ DB_LOGGING = str(env.get("DB_LOGGING", True))
37
+ DB_OPTIONS = str(env.get("DB_OPTIONS", None))
38
+
39
+ # Elasticsearch settings sourced from .env
40
+ ELASTIC_HOST = str(env.get("ELASTIC_HOST", "127.0.0.1"))
41
+ ELASTIC_PORT = int(env.get("ELASTIC_PORT", 9200))
42
+ ELASTIC_USER = str(env.get("ELASTIC_USER", None))
43
+ ELASTIC_PASS = str(env.get("ELASTIC_PASS", None))
44
+
45
+ # API Spec is built by Future from registered routes when OPENAPI.enabled is True.
46
+ # See config["OPENAPI"] and future.openapi.openapi_routes().
47
+ API_SPEC = {
48
+ "openapi": "3.0.0", # Updated to 3.0.0 for better compatibility
49
+ "info": {
50
+ "title": APP_NAME,
51
+ "version": APP_VERSION,
52
+ "description": APP_DESCRIPTION,
53
+ },
54
+ "paths": {},
55
+ # "servers": [
56
+ # {
57
+ # "url": APP_DOMAIN,
58
+ # "description": "Development server"
59
+ # }
60
+ # ]
61
+ }
62
+
63
+ # Logging configuration
64
+ LOGGING_CONFIG = {
65
+ "version": 1,
66
+ "disable_existing_loggers": False,
67
+ "formatters": {
68
+ "colored": {
69
+ "format": "%(message)s",
70
+ }
71
+ },
72
+ "handlers": {
73
+ "stdout": {
74
+ "class": "logging.StreamHandler",
75
+ "level": APP_LOG_LEVEL,
76
+ "formatter": "colored",
77
+ "stream": "ext://sys.stdout",
78
+ },
79
+ "queue_handler": {
80
+ "class": "logging.handlers.QueueHandler",
81
+ "handlers": [
82
+ "stdout",
83
+ ],
84
+ "respect_handler_level": True,
85
+ },
86
+ },
87
+ "loggers": {
88
+ "future": {
89
+ "level": APP_LOG_LEVEL,
90
+ "handlers": ["queue_handler"],
91
+ "propagate": False,
92
+ }
93
+ },
94
+ }
@@ -0,0 +1,14 @@
1
+ from future.tasks.scheduler import (
2
+ CronScheduler,
3
+ ScheduledTask,
4
+ Task,
5
+ Unit,
6
+ check_disk_usage,
7
+ check_dns,
8
+ check_http_status,
9
+ check_memory_usage,
10
+ check_ssh_banner,
11
+ check_system_uptime,
12
+ daily_backup,
13
+ sync_task_example,
14
+ )
@@ -0,0 +1,267 @@
1
+ import asyncio
2
+ import random
3
+
4
+ from datetime import datetime, timedelta
5
+ from enum import Enum
6
+ from typing import Any, Callable, Optional
7
+
8
+ from future.logger import log
9
+
10
+
11
+ class Unit(Enum):
12
+ SECONDS = "seconds"
13
+ MINUTES = "minutes"
14
+ HOURS = "hours"
15
+ DAYS = "days"
16
+
17
+
18
+ class Task:
19
+ def __init__(
20
+ self,
21
+ name: str,
22
+ interval: Optional[int] = None,
23
+ unit: Optional[Unit] = None,
24
+ start_time: Optional[datetime] = None,
25
+ kwargs: Optional[dict[str, Any]] = None,
26
+ func: Optional[Callable[..., Any]] = None,
27
+ args: tuple[Any, ...] = (),
28
+ jitter: Optional[float] = None,
29
+ ) -> None:
30
+ self.name = name
31
+ self.interval = interval
32
+ self.unit = unit
33
+ self.start_time = start_time
34
+ self.kwargs = kwargs or {}
35
+ self.func = func
36
+ self.args = args
37
+ self.jitter = jitter
38
+
39
+
40
+ class ScheduledTask:
41
+ """Represents a scheduled task with its timing configuration."""
42
+
43
+ def __init__(
44
+ self,
45
+ name: str,
46
+ func: Callable[..., Any],
47
+ interval: int,
48
+ unit: Unit,
49
+ start_time: Optional[datetime] = None,
50
+ last_run: Optional[datetime] = None,
51
+ next_run: Optional[datetime] = None,
52
+ args: tuple[Any, ...] = (),
53
+ kwargs: Optional[dict[str, Any]] = None,
54
+ jitter: Optional[float] = None,
55
+ ) -> None:
56
+ self.name = name
57
+ self.func = func
58
+ self.interval = interval
59
+ self.unit = unit
60
+ self.start_time = start_time
61
+ self.last_run = last_run
62
+ self.next_run = next_run
63
+ self.args = args
64
+ self.kwargs = kwargs if kwargs is not None else {}
65
+ self.jitter = jitter
66
+
67
+ if self.start_time is None:
68
+ self.start_time = datetime.now()
69
+ self.calculate_next_run()
70
+
71
+ def calculate_next_run(self) -> None:
72
+ """Calculate when this task should run next."""
73
+ if self.last_run is None:
74
+ self.next_run = self.start_time
75
+ else:
76
+ if self.unit == Unit.SECONDS:
77
+ self.next_run = self.last_run + timedelta(seconds=self.interval)
78
+ elif self.unit == Unit.MINUTES:
79
+ self.next_run = self.last_run + timedelta(minutes=self.interval)
80
+ elif self.unit == Unit.HOURS:
81
+ self.next_run = self.last_run + timedelta(hours=self.interval)
82
+ elif self.unit == Unit.DAYS:
83
+ self.next_run = self.last_run + timedelta(days=self.interval)
84
+ if self.next_run is not None and self.jitter and self.jitter > 0:
85
+ self.next_run = self.next_run + timedelta(seconds=random.uniform(0, self.jitter))
86
+
87
+
88
+ class CronScheduler:
89
+ """A cron-like scheduler for running background tasks."""
90
+
91
+ def __init__(self) -> None:
92
+ self.tasks: dict[str, ScheduledTask] = {}
93
+ self.running = False
94
+ self.check_interval = 1.0 # Check every second for tasks to run
95
+
96
+ def add_task(self, task: Task) -> None:
97
+ if task.func is None or task.interval is None or task.unit is None:
98
+ log.warning(f"Skipping task '{task.name}' - missing required parameters")
99
+ return
100
+
101
+ scheduled = ScheduledTask(
102
+ name=task.name,
103
+ func=task.func,
104
+ interval=task.interval,
105
+ unit=task.unit,
106
+ start_time=task.start_time,
107
+ args=task.args,
108
+ kwargs=task.kwargs,
109
+ jitter=task.jitter,
110
+ )
111
+ self.tasks[task.name] = scheduled
112
+ jitter_note = f" (jitter 0–{task.jitter}s)" if task.jitter else ""
113
+ log.info(f"Added scheduled task '{task.name}' to run every {task.interval} {task.unit.value}{jitter_note}")
114
+
115
+ def remove_task(self, name: str) -> bool:
116
+ """Remove a scheduled task."""
117
+ if name in self.tasks:
118
+ del self.tasks[name]
119
+ log.info(f"Removed scheduled task '{name}'")
120
+ return True
121
+ return False
122
+
123
+ def get_task(self, name: str) -> Optional[ScheduledTask]:
124
+ """Get a scheduled task by name."""
125
+ return self.tasks.get(name)
126
+
127
+ def list_tasks(self) -> list[str]:
128
+ """List all scheduled task names."""
129
+ return list(self.tasks.keys())
130
+
131
+ async def _run_task(self, task: ScheduledTask) -> None:
132
+ """Run a single task."""
133
+ try:
134
+ log.debug(f"Running scheduled task '{task.name}'")
135
+ if asyncio.iscoroutinefunction(task.func):
136
+ # Run async functions directly
137
+ await task.func(*task.args, **task.kwargs)
138
+ else:
139
+ # Run sync functions in thread pool
140
+ loop = asyncio.get_event_loop()
141
+ await loop.run_in_executor(None, task.func, *task.args, **task.kwargs)
142
+
143
+ task.last_run = datetime.now()
144
+ task.calculate_next_run()
145
+ if task.next_run:
146
+ log.debug(f"Completed scheduled task '{task.name}', next run at {task.next_run.strftime('%Y-%m-%d %H:%M:%S')}")
147
+ else:
148
+ log.debug(f"Completed scheduled task '{task.name}'")
149
+
150
+ except Exception as e:
151
+ log.error(f"Error running scheduled task '{task.name}': {e}")
152
+ # Don't update last_run on error, so it will retry next cycle
153
+
154
+ async def _scheduler_loop(self) -> None:
155
+ """Main scheduler loop that checks for tasks to run."""
156
+ log.info("Starting cron scheduler...")
157
+
158
+ while self.running:
159
+ now = datetime.now()
160
+ tasks_to_run = []
161
+
162
+ # Check which tasks need to run
163
+ for task in self.tasks.values():
164
+ if task.next_run and now >= task.next_run:
165
+ tasks_to_run.append(task)
166
+
167
+ # Run tasks that are due
168
+ if tasks_to_run:
169
+ log.debug(f"Running {len(tasks_to_run)} scheduled tasks")
170
+ for task in tasks_to_run:
171
+ asyncio.create_task(self._run_task(task))
172
+
173
+ # Wait before next check
174
+ await asyncio.sleep(self.check_interval)
175
+
176
+ log.info("Cron scheduler stopped")
177
+
178
+ async def start(self) -> None:
179
+ """Start the scheduler."""
180
+ if not self.running:
181
+ self.running = True
182
+ asyncio.create_task(self._scheduler_loop())
183
+
184
+ async def stop(self) -> None:
185
+ """Stop the scheduler."""
186
+ self.running = False
187
+
188
+
189
+ # --- Minimal cron task functions ---
190
+ async def check_dns(domain: str = "example.com") -> None:
191
+ import socket
192
+
193
+ try:
194
+ ip = socket.gethostbyname(domain)
195
+ log.info(f"DNS: {domain} -> {ip}")
196
+ except socket.gaierror as e:
197
+ log.error(f"DNS lookup failed for {domain}: {e}")
198
+
199
+
200
+ async def check_http_status(url: str = "https://httpbin.org/status/200") -> None:
201
+ import httpx
202
+
203
+ try:
204
+ async with httpx.AsyncClient() as client:
205
+ response = await client.get(url)
206
+ log.info(f"HTTP: {url} -> {response.status_code}")
207
+ except Exception as e:
208
+ log.error(f"HTTP check failed for {url}: {e}")
209
+
210
+
211
+ async def check_ssh_banner(host: str = "localhost", port: int = 22) -> None:
212
+ try:
213
+ reader, writer = await asyncio.open_connection(host, port)
214
+ banner = await reader.read(1024)
215
+ log.info(f"SSH banner for {host}:{port}: {banner.decode(errors='ignore').strip()}")
216
+ writer.close()
217
+ await writer.wait_closed()
218
+ except Exception as e:
219
+ log.error(f"SSH banner check failed for {host}:{port}: {e}")
220
+
221
+
222
+ def check_system_uptime() -> None:
223
+ import subprocess
224
+
225
+ try:
226
+ result = subprocess.run(["uptime"], capture_output=True, text=True, timeout=5.0)
227
+ if result.returncode == 0:
228
+ log.info(f"System uptime: {result.stdout.strip()}")
229
+ else:
230
+ log.error(f"Uptime command failed: {result.stderr}")
231
+ except subprocess.TimeoutExpired:
232
+ log.error("Uptime command timed out")
233
+ except Exception as e:
234
+ log.error(f"Error checking system uptime: {e}")
235
+
236
+
237
+ def check_disk_usage(path: str = "/") -> None:
238
+ import shutil
239
+
240
+ try:
241
+ total, used, _ = shutil.disk_usage(path)
242
+ used_percent = (used / total) * 100
243
+ log.info(f"Disk usage for {path}: {used_percent:.1f}% used ({used // (1024**3)}GB / {total // (1024**3)}GB)")
244
+ except Exception as e:
245
+ log.error(f"Error checking disk usage for {path}: {e}")
246
+
247
+
248
+ def check_memory_usage() -> None:
249
+ import psutil
250
+
251
+ try:
252
+ memory = psutil.virtual_memory()
253
+ log.info(f"Memory usage: {memory.percent}% used ({memory.used // (1024**3)}GB / {memory.total // (1024**3)}GB)")
254
+ except ImportError:
255
+ log.warning("psutil not available, skipping memory check")
256
+ except Exception as e:
257
+ log.error(f"Error checking memory usage: {e}")
258
+
259
+
260
+ def sync_task_example() -> None:
261
+ log.info(f"Running sync task at {datetime.now()}")
262
+
263
+
264
+ async def daily_backup() -> None:
265
+ log.info(f"Running daily backup at {datetime.now()}")
266
+ # TODO: Implement actual backup logic
267
+ # Example: backup database, files, etc.
@@ -0,0 +1 @@
1
+ from future.testing.client import FutureTestClient
@@ -0,0 +1,135 @@
1
+ from typing import Any
2
+ from urllib.parse import urlparse
3
+
4
+ import asyncio
5
+ import httpx
6
+
7
+ from future.application import Future
8
+
9
+
10
+ class FutureTestClient:
11
+ def __init__(self, app: Future) -> None:
12
+ self.app = app
13
+ self.transport = httpx.ASGITransport(app=self.app)
14
+ self.client = httpx.AsyncClient(transport=self.transport)
15
+
16
+ async def __aenter__(self) -> "FutureTestClient":
17
+ await self.client.__aenter__()
18
+ return self
19
+
20
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
21
+ await self.client.__aexit__(exc_type, exc, tb)
22
+
23
+ async def get(self, url: str, headers: dict[str, str] | None = None) -> httpx.Response:
24
+ response = await self.client.get(url, headers=headers)
25
+ return response
26
+
27
+ async def post(self, url: str, json: dict[str, Any] | None = None, data: str | bytes | None = None, headers: dict[str, str] | None = None) -> httpx.Response:
28
+ if json is not None:
29
+ response = await self.client.post(url, json=json, headers=headers)
30
+ elif data is not None:
31
+ response = await self.client.post(url, content=data, headers=headers)
32
+ else:
33
+ response = await self.client.post(url, headers=headers)
34
+ return response
35
+
36
+ async def websocket_connect(self, url: str, headers: dict[str, str] | None = None) -> Any:
37
+ """Connect via real ASGI websocket handshake against the Future app."""
38
+ parsed = urlparse(url)
39
+ path = parsed.path
40
+ if parsed.query:
41
+ path += "?" + parsed.query
42
+
43
+ ws_headers = []
44
+ if headers:
45
+ for key, value in headers.items():
46
+ ws_headers.append((key.lower().encode(), value.encode()))
47
+
48
+ scope = {
49
+ "type": "websocket",
50
+ "asgi": {"version": "3.0", "spec_version": "2.3"},
51
+ "path": path,
52
+ "headers": ws_headers,
53
+ "raw_path": path.encode(),
54
+ "query_string": b"",
55
+ "client": ("127.0.0.1", 12345),
56
+ "server": ("127.0.0.1", 8000),
57
+ "scheme": "ws",
58
+ "root_path": "",
59
+ "subprotocols": [],
60
+ "extensions": {},
61
+ "state": {},
62
+ }
63
+
64
+ send_queue: asyncio.Queue = asyncio.Queue()
65
+ recv_queue: asyncio.Queue = asyncio.Queue()
66
+ await recv_queue.put({"type": "websocket.connect"})
67
+
68
+ async def receive() -> dict[str, Any]:
69
+ return await recv_queue.get()
70
+
71
+ async def send(message: dict[str, Any]) -> None:
72
+ await send_queue.put(message)
73
+
74
+ task = asyncio.create_task(self.app(scope, receive, send))
75
+
76
+ class AsgiWebSocket:
77
+ def __init__(self) -> None:
78
+ self.accepted = False
79
+ self.closed = False
80
+ self.close_code = None
81
+ self.close_reason = None
82
+ self._task = task
83
+
84
+ async def _next_server_message(self) -> dict[str, Any]:
85
+ while True:
86
+ try:
87
+ return await asyncio.wait_for(send_queue.get(), timeout=2.0)
88
+ except asyncio.TimeoutError:
89
+ if self._task.done():
90
+ exc = self._task.exception()
91
+ if exc:
92
+ raise exc
93
+ raise TimeoutError("WebSocket app ended without a message")
94
+ raise
95
+
96
+ async def wait_accepted(self) -> None:
97
+ message = await self._next_server_message()
98
+ if message["type"] == "websocket.close":
99
+ self.closed = True
100
+ self.close_code = message.get("code")
101
+ self.close_reason = message.get("reason")
102
+ raise ConnectionError(f"WebSocket closed: {self.close_code} {self.close_reason}")
103
+ if message["type"] != "websocket.accept":
104
+ raise AssertionError(f"Expected websocket.accept, got {message}")
105
+ self.accepted = True
106
+
107
+ async def send(self, message: str) -> None:
108
+ await recv_queue.put({"type": "websocket.receive", "text": message})
109
+
110
+ async def recv(self) -> str:
111
+ message = await self._next_server_message()
112
+ if message["type"] == "websocket.send":
113
+ if "text" in message:
114
+ return message["text"]
115
+ return message.get("bytes", b"").decode("utf-8")
116
+ if message["type"] == "websocket.close":
117
+ self.closed = True
118
+ self.close_code = message.get("code")
119
+ self.close_reason = message.get("reason")
120
+ raise ConnectionError(f"WebSocket closed: {self.close_code} {self.close_reason}")
121
+ raise AssertionError(f"Unexpected message: {message}")
122
+
123
+ async def close(self) -> None:
124
+ await recv_queue.put({"type": "websocket.disconnect", "code": 1000})
125
+ try:
126
+ await asyncio.wait_for(self._task, timeout=2.0)
127
+ except asyncio.TimeoutError:
128
+ self._task.cancel()
129
+
130
+ socket = AsgiWebSocket()
131
+ await socket.wait_accepted()
132
+ # Greeting frame from WebSocketResponse (duplex send/recv available after)
133
+ first = await socket.recv()
134
+ socket._first = first # type: ignore[attr-defined]
135
+ return socket
future/types.py ADDED
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import re
5
+
6
+ from collections.abc import Awaitable, MutableMapping
7
+ from typing import Any, Callable, TypedDict
8
+
9
+
10
+ # ASGI specific
11
+ ASGIScope = MutableMapping[str, Any]
12
+ ASGIMessage = MutableMapping[str, Any]
13
+ ASGISend = Callable[[ASGIMessage], Awaitable[None]]
14
+ ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
15
+
16
+
17
+ class AsgiEventType(enum.StrEnum):
18
+ LIFESPAN_STARTUP = "lifespan.startup"
19
+ LIFESPAN_STARTUP_COMPLETE = "lifespan.startup.complete"
20
+ LIFESPAN_STARTUP_FAILED = "lifespan.startup.failed"
21
+ LIFESPAN_SHUTDOWN = "lifespan.shutdown"
22
+ LIFESPAN_SHUTDOWN_COMPLETE = "lifespan.shutdown.complete"
23
+ LIFESPAN_SHUTDOWN_FAILED = "lifespan.shutdown.failed"
24
+ HTTP_REQUEST = "http.request"
25
+ HTTP_RESPONSE_START = "http.response.start"
26
+ HTTP_RESPONSE_BODY = "http.response.body"
27
+ WEBSOCKET_CONNECT = "websocket.connect"
28
+ WEBSOCKET_ACCEPT = "websocket.accept"
29
+ WEBSOCKET_RECEIVE = "websocket.receive"
30
+ WEBSOCKET_SEND = "websocket.send"
31
+ WEBSOCKET_DISCONNECT = "websocket.disconnect"
32
+ WEBSOCKET_CLOSE = "websocket.close"
33
+
34
+
35
+ class RegexConfig(TypedDict):
36
+ paths: list[re.Pattern[str]]
37
+
38
+
39
+ class RouteConfig(TypedDict):
40
+ handler: Callable[..., Any]
41
+ controller: Any
42
+ action: str | None
43
+ middleware: dict[str, list[Any]] # type: ignore
44
+ regex: dict[str, list[re.Pattern[str] | re.Pattern[bytes]]] | None
45
+ methods: list[str]
46
+ route: Any # FIXME: Added in temporarily because of a regression in the regex matching causing /users/123/test to not match /users/123/test/. Not sure when this happened. This shouldnt be needed.
47
+ group: dict[str, str]
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: future-framework
3
+ Version: 1.1.0
4
+ Summary: Next Gen. ASGI Framework for minimal Web APIs
5
+ License: LICENSE
6
+ License-File: LICENSE
7
+ Author: nicolaipre
8
+ Requires-Python: >=3.12,<4.0
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: clickhouse-driver (>=0.2.11,<0.3.0)
15
+ Requires-Dist: elasticsearch (>=9.4.0,<10.0.0)
16
+ Requires-Dist: faker (>=40.0.0,<41.0.0)
17
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
18
+ Requires-Dist: inflection (>=0.5.1,<0.6.0)
19
+ Requires-Dist: psycopg2-binary (>=2.9.12,<3.0.0)
20
+ Requires-Dist: pymongo (>=4.17.0,<5.0.0)
21
+ Requires-Dist: pymysql (>=1.2.0,<2.0.0)
22
+ Requires-Dist: python-dotenv (>=1.2.2,<2.0.0)
23
+ Requires-Dist: redis (>=8.0.0,<9.0.0)
24
+ Requires-Dist: rich (>=15.0.0,<16.0.0)
25
+ Requires-Dist: sqlalchemy (>=2.0.51,<3.0.0)
26
+ Requires-Dist: strawberry-graphql (>=0.323.0,<0.324.0)
27
+ Requires-Dist: uvicorn (>=0.51.0,<0.52.0)
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Future
31
+ Minimal, decorator-free [ASGI](https://asgi.readthedocs.io/) framework for Python APIs — routing, middleware, Active Record, migrations, seeds, OpenAPI, and interval tasks.
32
+
33
+ [![Build Status](https://github.com/nicolaipre/future/actions/workflows/ci.yml/badge.svg)](https://github.com/nicolaipre/future/actions)
34
+ [![Package version](https://badge.fury.io/py/future-framework.svg)](https://pypi.org/project/future-framework/)
35
+ [![codecov](https://codecov.io/gh/nicolaipre/future/branch/master/graph/badge.svg)](https://codecov.io/gh/nicolaipre/future)
36
+
37
+ ## Documentation
38
+ Published at **[nicolaipre.github.io/future](https://nicolaipre.github.io/future/)**. To preview locally: `poetry install --with docs && poetry run mkdocs serve`.
39
+
40
+ ## Install
41
+ ```bash
42
+ poetry add future-framework
43
+ # or from git: poetry add git+https://github.com/nicolaipre/future.git@master
44
+ poetry run future init myproject
45
+ ```
46
+
47
+ Import name is `future` (`from future.application import Future`).
48
+
49
+ ## Hello
50
+ ```python
51
+ from future.application import Future
52
+ from future.controllers import Controller
53
+ from future.lifespan import Lifespan
54
+ from future.response import Response
55
+ from future.routing import Get, RouteGroup
56
+
57
+ class HomeController(Controller):
58
+ async def index(self) -> Response:
59
+ return self.response.json({"ok": True})
60
+
61
+ app = Future(lifespan=Lifespan([], [], []), config={"APP_DOMAIN": "", "APP_NAME": "Demo"})
62
+ app.add_routes([RouteGroup(name="Main", routes=[Get("/", HomeController.index, "home")])])
63
+ app.run(host="127.0.0.1", port=8000)
64
+ ```
65
+
66
+ ## License
67
+ See [LICENSE](LICENSE). Versioning: [CHANGELOG.md](CHANGELOG.md).
68
+