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
@@ -0,0 +1,9 @@
1
+ """
2
+ Azure AD authentication.
3
+ """
4
+
5
+ from future.authentication.Authentication import BaseAuthentication
6
+
7
+
8
+ class AzureADAuthentication(BaseAuthentication):
9
+ auth_type = "basic"
@@ -0,0 +1,9 @@
1
+ """
2
+ Regular username & password authentication (check against SQL database, generating a JWT).
3
+ """
4
+
5
+ from future.authentication.Authentication import Authentication
6
+
7
+
8
+ class BasicAuthentication(Authentication):
9
+ auth_type = "basic"
@@ -0,0 +1,9 @@
1
+ """
2
+ Kerberos Single Sign-On authentication.
3
+ """
4
+
5
+ from future.authentication.Authentication import BaseAuthentication
6
+
7
+
8
+ class KerberosAuthentication(BaseAuthentication):
9
+ auth_type = "sso"
@@ -0,0 +1,9 @@
1
+ """
2
+ Keycloak authentication.
3
+ """
4
+
5
+ from future.authentication.Authentication import Authentication
6
+
7
+
8
+ class KeycloakAuthentication(Authentication):
9
+ auth_type = "oauth"
@@ -0,0 +1,9 @@
1
+ """
2
+ OAuth 2 authentication.
3
+ """
4
+
5
+ from future.authentication.Authentication import Authentication
6
+
7
+
8
+ class OAuth2Authentication(Authentication):
9
+ auth_type = "oauth"
@@ -0,0 +1,9 @@
1
+ """
2
+ OpenID Connect authentication.
3
+ """
4
+
5
+ from future.authentication.Authentication import Authentication
6
+
7
+
8
+ class OpenIdConnectAuthentication(Authentication):
9
+ auth_type = "oidc"
@@ -0,0 +1,9 @@
1
+ """
2
+ SAML Single Sign-On authentication.
3
+ """
4
+
5
+ from future.authentication.Authentication import Authentication
6
+
7
+
8
+ class SAMLAuthentication(Authentication):
9
+ auth_type = "sso"
future/cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from future.cli.main import main
future/cli/main.py ADDED
@@ -0,0 +1,608 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ # Boilerplate templates for `future init`
10
+ APP_ROOT = "app"
11
+ APP_DIRS = ["config", "controllers", "middleware", "models", "plugins", "tasks"]
12
+ PROJECT_FILES = [".env.example", ".gitignore", "LICENSE", "pyproject.toml", "README.md"]
13
+ README_MD = """\
14
+ # Future boilerplate
15
+
16
+ Scaffolded with `future init`.
17
+
18
+ ```bash
19
+ cp .env.example .env # DB_DATABASE=database → database.sqlite
20
+ poetry install
21
+ poetry run future make:migration ExampleModel
22
+ poetry run future migrate
23
+ poetry run future make:seed ExampleModel
24
+ poetry run future seed
25
+ poetry run python run.py
26
+ poetry run future routes
27
+ ```
28
+ """
29
+ ENV_EXAMPLE = """\
30
+ # Application settings
31
+ APP_NAME=Example
32
+ APP_VERSION=1.0
33
+ APP_DESCRIPTION=Description
34
+ APP_DOMAIN=example.com
35
+ APP_HOST=127.0.0.1
36
+ APP_PORT=8000
37
+ APP_DEBUG=True
38
+ APP_ACCESS_LOG=True
39
+ APP_WORKERS=1
40
+ APP_KEY=REPLACE_WITH_GENERATED_SECRET_KEY
41
+
42
+ # Database settings (SQLite by default — no server required)
43
+ DB_DATABASE=database
44
+
45
+ # Optional MySQL (switch app/config/Database.py to MySQL and set these)
46
+ # DB_HOST=127.0.0.1
47
+ # DB_PORT=3306
48
+ # DB_DATABASE=future
49
+ # DB_USERNAME=root
50
+ # DB_PASSWORD=
51
+ """
52
+ PYPROJECT_TOML = """\
53
+ [tool.poetry]
54
+ name = "future-boilerplate"
55
+ version = "0.0.1"
56
+ description = "Boilerplate app for the Future framework."
57
+ authors = ["nicolaipre"]
58
+ readme = "README.md"
59
+
60
+ [tool.poetry.dependencies]
61
+ python = "^3.12"
62
+ python-dotenv = "^1.0.1"
63
+ future-framework = { git = "https://github.com/nicolaipre/future.git", rev = "master" }
64
+ # Or from PyPI after publish: future-framework = "^1.1.0"
65
+ # Or, while developing Future next to this app:
66
+ # future-framework = { path = "../future", develop = true }
67
+
68
+ [build-system]
69
+ requires = ["poetry-core"]
70
+ build-backend = "poetry.core.masonry.api"
71
+ """
72
+ LICENSE = """\
73
+ MIT License
74
+
75
+ Copyright (c) 2025 nicolaipre
76
+
77
+ Permission is hereby granted, free of charge, to any person obtaining a copy
78
+ of this software and associated documentation files (the \"Software\"), to deal
79
+ in the Software without restriction, including without limitation the rights
80
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
81
+ copies of the Software, and to permit persons to whom the Software is
82
+ furnished to do so, subject to the following conditions:
83
+
84
+ The above copyright notice and this permission notice shall be included in all
85
+ copies or substantial portions of the Software.
86
+
87
+ THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
88
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
89
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
90
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
91
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
92
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
93
+ SOFTWARE.
94
+ """
95
+ GITIGNORE = ".idea/\n.vscode/\n.env\n.venv\npoetry.lock\n__pycache__/\n*.sqlite\n*.sqlite3\n"
96
+
97
+ APP_ROUTES = """\
98
+ from app.controllers.ExampleController import ExampleController
99
+ from app.middleware.ExampleMiddleware import ExampleMiddleware
100
+ from future.openapi import openapi_routes
101
+ from future.routing import RouteGroup, Get
102
+
103
+
104
+ routes = [
105
+ RouteGroup(
106
+ name="Docs",
107
+ prefix="/api",
108
+ # middlewares=[ExampleMiddleware], # protect docs if needed
109
+ routes=openapi_routes(uis=["swagger", "redoc", "scalar", "rapidoc"]),
110
+ ),
111
+ RouteGroup(
112
+ name="Main",
113
+ middlewares=[ExampleMiddleware],
114
+ routes=[
115
+ Get("/", ExampleController.index, "home"),
116
+ Get("/examples", ExampleController.index, "examples"),
117
+ Get("/examples/<id>", ExampleController.show, "example"),
118
+ ],
119
+ ),
120
+ ]
121
+ """
122
+
123
+ EXAMPLE_PLUGIN = """\
124
+ from future.plugins import Plugin
125
+
126
+
127
+ class ExamplePlugin(Plugin):
128
+ pass
129
+ """
130
+
131
+ EXAMPLE_MODEL = """\
132
+ from future.models import Model
133
+ from datetime import datetime
134
+
135
+
136
+ class ExampleModel(Model):
137
+ __connection__ = "default"
138
+ # __table__ = "example_models" # optional; otherwise tableized class name
139
+
140
+ id: str
141
+ name: str
142
+ description: str
143
+ created_at: datetime
144
+ updated_at: datetime
145
+ """
146
+
147
+ EXAMPLE_MIDDLEWARE = """\
148
+ from future.middleware import Middleware
149
+ from future.response import Response
150
+ from typing import Optional
151
+
152
+
153
+ class ExampleMiddleware(Middleware):
154
+ name = "ExampleMiddleware"
155
+ priority = 0
156
+
157
+ async def before(self) -> Optional[Response]:
158
+ return None
159
+
160
+ async def after(self) -> Optional[Response]:
161
+ return None
162
+ """
163
+
164
+ EXAMPLE_CONTROLLER = """\
165
+ from future.controllers import Controller
166
+ from future.response import Response
167
+ from app.models.ExampleModel import ExampleModel
168
+
169
+
170
+ class ExampleController(Controller):
171
+ async def index(self) -> Response:
172
+ examples = ExampleModel.all()
173
+ return self.response.json([example.to_dict() for example in examples], status=200)
174
+
175
+ async def show(self, id: str) -> Response:
176
+ example = ExampleModel.find(id)
177
+ if example is None:
178
+ return self.response.json({"error": "Not found"}, status=404)
179
+ return self.response.json(example.to_dict(), status=200)
180
+ """
181
+
182
+ EXAMPLE_TASK = """\
183
+ \"\"\"
184
+ Example task — pass to Lifespan cron_tasks via Task(..., func=run).
185
+ \"\"\"
186
+
187
+
188
+ def run():
189
+ pass
190
+ """
191
+
192
+ CONFIG_DATABASE = """\
193
+ from future.databases.SQLite import SQLite
194
+ from app.config.Settings import DB_DATABASE
195
+
196
+ # Default driver: SQLite (local file). Swap in MySQL when you need a server.
197
+ # Future(config={"DATABASES": DATABASES}) registers Connections at boot.
198
+ DATABASES = {
199
+ "default": "sqlite",
200
+ "sqlite": SQLite(database=DB_DATABASE),
201
+ }
202
+ """
203
+
204
+ CONFIG_SETTINGS = """\
205
+ from os import environ as env
206
+ from dotenv import load_dotenv
207
+
208
+ load_dotenv(dotenv_path=".env")
209
+
210
+ # Application settings sourced from .env
211
+ APP_NAME = str(env.get("APP_NAME", "Future"))
212
+ APP_VERSION = str(env.get("APP_VERSION", "1.0"))
213
+ APP_DESCRIPTION = str(env.get("APP_DESCRIPTION", "A short description"))
214
+ APP_DEBUG = env.get("APP_DEBUG", "False").lower() == "true"
215
+ APP_LOG_LEVEL = str("DEBUG" if APP_DEBUG else env.get("APP_LOG_LEVEL", "INFO"))
216
+ APP_ACCESS_LOG = env.get("APP_ACCESS_LOG", "False").lower() == "true"
217
+ APP_WORKERS = int(env.get("APP_WORKERS", 1))
218
+ APP_HOST = str(env.get("APP_HOST", "127.0.0.1"))
219
+ APP_PORT = int(env.get("APP_PORT", 8000))
220
+ APP_KEY = str(env.get("APP_KEY", "secret"))
221
+ APP_DOMAIN = str(env.get("APP_DOMAIN", "example.com"))
222
+
223
+ # Database settings sourced from .env (name only; SQLite appends .sqlite)
224
+ DB_DATABASE = str(env.get("DB_DATABASE", "database"))
225
+ DB_HOST = str(env.get("DB_HOST", "127.0.0.1"))
226
+ DB_PORT = int(env.get("DB_PORT", 3306))
227
+ DB_USERNAME = str(env.get("DB_USERNAME", "root"))
228
+ DB_PASSWORD = str(env.get("DB_PASSWORD", ""))
229
+ DB_LOGGING = env.get("DB_LOGGING", "False").lower() == "true"
230
+ """
231
+
232
+ RUN_PY = """\
233
+ from app.config.Settings import APP_HOST, APP_PORT, APP_DEBUG, APP_WORKERS, APP_NAME, APP_DOMAIN
234
+ from app.config.Database import DATABASES
235
+ from app.routes import routes
236
+ from future.application import Future
237
+ from future.lifespan import Lifespan
238
+
239
+ # Tasks that will run on startup:
240
+ startup_tasks = [
241
+ # Task()
242
+ ]
243
+
244
+ # Tasks that will run on shutdown:
245
+ shutdown_tasks = [
246
+ # Task()
247
+ ]
248
+
249
+ # Tasks that will run with intervals using a cron-like scheduler:
250
+ cron_tasks = [
251
+ # Task("Example", interval=1, unit=Unit.HOURS, func=run),
252
+ ]
253
+
254
+ config = {
255
+ "APP_DOMAIN": APP_DOMAIN,
256
+ "APP_NAME": APP_NAME,
257
+ "APP_HOST": APP_HOST,
258
+ "APP_PORT": APP_PORT,
259
+ "APP_DEBUG": APP_DEBUG,
260
+ "DATABASES": DATABASES,
261
+ "OPENAPI": {
262
+ "enabled": True,
263
+ "uis": ["swagger", "redoc", "scalar", "rapidoc"],
264
+ "auto_routes": False,
265
+ "path_prefix": "",
266
+ # "redocly_license_key": "", # paid Reference Docs (Try it); omit for OSS ReDoc
267
+ },
268
+ }
269
+
270
+ lifespan = Lifespan(startup_tasks=startup_tasks, shutdown_tasks=shutdown_tasks, cron_tasks=cron_tasks)
271
+ app = Future(lifespan=lifespan, config=config)
272
+ app.add_routes(routes)
273
+
274
+ if __name__ == "__main__":
275
+ app.run(host=APP_HOST, port=APP_PORT, workers=APP_WORKERS)
276
+ """
277
+
278
+
279
+ def get_app_from_project() -> Any:
280
+ """Dynamically import the app from the current project."""
281
+ import importlib.util
282
+
283
+ # Add current directory to Python path for scaffolded projects
284
+ if os.getcwd() not in sys.path:
285
+ sys.path.insert(0, os.getcwd())
286
+
287
+ # Try to import from run.py
288
+ if os.path.exists("run.py"):
289
+ spec = importlib.util.spec_from_file_location("run", "run.py")
290
+ if spec and spec.loader:
291
+ module = importlib.util.module_from_spec(spec)
292
+ spec.loader.exec_module(module)
293
+ if hasattr(module, "app"):
294
+ return module.app
295
+
296
+ print("Error: Could not find app instance. Make sure you're in a Future project directory.")
297
+ print("Expected to find app in: run.py")
298
+ sys.exit(1)
299
+
300
+
301
+ def print_routes() -> None:
302
+ from rich.box import SIMPLE_HEAD
303
+ from rich.console import Console
304
+ from rich.table import Table
305
+ from rich.text import Text
306
+
307
+ console = Console()
308
+ app = get_app_from_project()
309
+ routes = getattr(app, "routes", {})
310
+ if not routes:
311
+ console.print("[yellow]No routes registered.[/yellow]")
312
+ return
313
+
314
+ # (domain, group_name, prefix, subdomain, middleware_label) -> rows
315
+ sections: dict[tuple[str, str, str, str, str], list[tuple[str, str, str, str]]] = {}
316
+ for domain, domain_routes in routes.items():
317
+ domain_label = domain or "(any host)"
318
+ for route_key, route_info in domain_routes.items():
319
+ route = route_info.get("route")
320
+ path = getattr(route, "path", None) or (route_key.split(" ", 1)[-1] if " " in str(route_key) else route_key)
321
+ methods = route_info.get("methods") or (["WEBSOCKET"] if "ws" in path else ["GET"])
322
+ name = getattr(route, "name", "") or ""
323
+ group = route_info.get("group") or {}
324
+ group_name = group.get("name") or "(ungrouped)"
325
+ prefix = group.get("prefix") or ""
326
+ group_sub = group.get("subdomain") or ""
327
+ middleware_classes = route_info.get("middleware", {}).get("classes", [])
328
+ middleware_names = []
329
+ for middleware in middleware_classes:
330
+ if hasattr(middleware, "name") and middleware.name:
331
+ middleware_names.append(middleware.name)
332
+ else:
333
+ middleware_names.append(middleware.__name__)
334
+ middleware_label = ", ".join(middleware_names) if middleware_names else "(none)"
335
+ param_names = getattr(route, "param_names", None) or []
336
+ args = ", ".join(param_names) if param_names else "-"
337
+ section_key = (domain_label, group_name, prefix, group_sub, middleware_label)
338
+ sections.setdefault(section_key, []).append((",".join(methods), path, name, args))
339
+
340
+ console.print()
341
+ console.print(Text("Routes", style="bold"))
342
+ total = 0
343
+ for (domain_label, group_name, prefix, group_sub, middleware_label), rows in sorted(sections.items(), key=lambda item: (item[0][0], item[0][1], item[0][2])):
344
+ rows.sort(key=lambda row: (row[1], row[0]))
345
+ total += len(rows)
346
+ meta_parts = [f"domain={domain_label}"]
347
+ if prefix:
348
+ meta_parts.append(f"prefix={prefix}")
349
+ if group_sub:
350
+ meta_parts.append(f"subdomain={group_sub}")
351
+ meta_parts.append(f"middleware={middleware_label}")
352
+ console.print()
353
+ console.print(Text(f"{group_name}", style="bold cyan"), Text(f" {' · '.join(meta_parts)}", style="dim"))
354
+ table = Table(box=SIMPLE_HEAD, show_header=True, pad_edge=False, expand=False)
355
+ table.add_column("Method", style="magenta", no_wrap=True)
356
+ table.add_column("Path", style="green")
357
+ table.add_column("Name", style="white")
358
+ table.add_column("Args", style="yellow")
359
+ for method, path, name, args in rows:
360
+ table.add_row(method, path, name, args)
361
+ console.print(table)
362
+
363
+ console.print()
364
+ console.print(Text(f"{total} route(s) in {len(sections)} group(s)", style="dim"))
365
+ console.print()
366
+
367
+
368
+ def scaffold_project(target_dir: str) -> None:
369
+ target = Path(target_dir)
370
+ if target.exists() and any(target.iterdir()):
371
+ print(f"Directory {target} already exists and is not empty.")
372
+ sys.exit(1)
373
+
374
+ # Create main app directory
375
+ app_dir = target / APP_ROOT
376
+ app_dir.mkdir(parents=True, exist_ok=True)
377
+
378
+ # Create subdirectories
379
+ for d in APP_DIRS:
380
+ (app_dir / d).mkdir(parents=True, exist_ok=True)
381
+
382
+ # Create root level files
383
+ (target / "README.md").write_text(README_MD)
384
+ (target / ".env.example").write_text(ENV_EXAMPLE)
385
+ (target / "pyproject.toml").write_text(PYPROJECT_TOML)
386
+ (target / "LICENSE").write_text(LICENSE)
387
+ (target / ".gitignore").write_text(GITIGNORE)
388
+ (target / "run.py").write_text(RUN_PY)
389
+
390
+ # Create app-specific files
391
+ (app_dir / "routes.py").write_text(APP_ROUTES)
392
+ (app_dir / "config" / "Settings.py").write_text(CONFIG_SETTINGS)
393
+ (app_dir / "config" / "Database.py").write_text(CONFIG_DATABASE)
394
+ (target / "database" / "migrations").mkdir(parents=True, exist_ok=True)
395
+ (target / "database" / "seeds").mkdir(parents=True, exist_ok=True)
396
+ (app_dir / "controllers" / "ExampleController.py").write_text(EXAMPLE_CONTROLLER)
397
+ (app_dir / "middleware" / "ExampleMiddleware.py").write_text(EXAMPLE_MIDDLEWARE)
398
+ (app_dir / "models" / "ExampleModel.py").write_text(EXAMPLE_MODEL)
399
+ (app_dir / "plugins" / "ExamplePlugin.py").write_text(EXAMPLE_PLUGIN)
400
+ (app_dir / "tasks" / "ExampleTask.py").write_text(EXAMPLE_TASK)
401
+
402
+ print(f"Scaffolded new Future project at {target.resolve()}")
403
+
404
+
405
+ def ensure_project_path() -> None:
406
+ if os.getcwd() not in sys.path:
407
+ sys.path.insert(0, os.getcwd())
408
+
409
+
410
+ def register_database_connections() -> None:
411
+ get_app_from_project()
412
+
413
+
414
+ def run_migrations(rollback: bool = False) -> None:
415
+ from future.migrations.Migrator import Migrator
416
+ from rich.console import Console
417
+ from rich.text import Text
418
+
419
+ register_database_connections()
420
+ migrator = Migrator(path="database/migrations")
421
+ console = Console()
422
+ if rollback:
423
+ rolled = migrator.rollback()
424
+ if not rolled:
425
+ console.print(Text("✓ Nothing to rollback.", style="green"))
426
+ return
427
+ for name in rolled:
428
+ line = Text()
429
+ line.append("✓ ", style="green")
430
+ line.append("Rolled back ", style="green")
431
+ line.append(name, style="blue")
432
+ console.print(line)
433
+ return
434
+ ran = migrator.run()
435
+ if not ran:
436
+ console.print(Text("✓ Nothing to migrate.", style="green"))
437
+ return
438
+ for name in ran:
439
+ line = Text()
440
+ line.append("✓ ", style="green")
441
+ line.append("Migrated ", style="green")
442
+ line.append(name, style="blue")
443
+ console.print(line)
444
+
445
+
446
+ def run_seeds(seeder: str | None = None) -> None:
447
+ from future.seeds.SeedRunner import SeedRunner
448
+
449
+ register_database_connections()
450
+ ran = SeedRunner(path="database/seeds").run(name=seeder)
451
+ print(f"Seeded: {ran}")
452
+
453
+
454
+ def make_migration(model_name: str | None = None) -> None:
455
+ from future.migrations.MigrationGenerator import MigrationGenerator
456
+ from rich.console import Console
457
+ from rich.text import Text
458
+
459
+ ensure_project_path()
460
+ console = Console()
461
+ try:
462
+ paths = MigrationGenerator().make(model_name)
463
+ except (FileNotFoundError, ValueError) as error:
464
+ print(f"Error: {error}")
465
+ sys.exit(1)
466
+ if not paths:
467
+ print("No migrations created (no annotated models found).")
468
+ return
469
+ for path in paths:
470
+ line = Text()
471
+ line.append("✓ ", style="green")
472
+ line.append("Migration created successfully! ", style="green")
473
+ line.append(str(path), style="blue")
474
+ console.print(line)
475
+
476
+
477
+ def make_seed(model_name: str | None = None) -> None:
478
+ from future.seeds.SeedGenerator import SeedGenerator
479
+
480
+ ensure_project_path()
481
+ try:
482
+ paths = SeedGenerator().make(model_name)
483
+ except (FileNotFoundError, FileExistsError, ValueError) as error:
484
+ print(f"Error: {error}")
485
+ sys.exit(1)
486
+ if not paths:
487
+ print("No new seeders created (all models already have seeders, or no annotated models found).")
488
+ return
489
+ for path in paths:
490
+ print(f"Created: {path}")
491
+
492
+
493
+ def make_stub(kind: str, name: str) -> None:
494
+ from .stubs import StubMaker
495
+
496
+ ensure_project_path()
497
+ try:
498
+ path = StubMaker().make(kind, name)
499
+ except (FileNotFoundError, FileExistsError, ValueError) as error:
500
+ print(f"Error: {error}")
501
+ sys.exit(1)
502
+ print(f"Created: {path}")
503
+
504
+
505
+ def run_app(host: str | None = None, port: int | None = None, workers: int | None = None) -> None:
506
+ app = get_app_from_project()
507
+ config = getattr(app, "config", {}) or {}
508
+ bind_host = host if host is not None else config.get("APP_HOST", "127.0.0.1")
509
+ bind_port = port if port is not None else int(config.get("APP_PORT", 8000))
510
+ bind_workers = workers if workers is not None else int(config.get("APP_WORKERS", 1))
511
+ app.run(host=bind_host, port=bind_port, workers=bind_workers)
512
+
513
+
514
+ def main() -> None:
515
+ parser = argparse.ArgumentParser(
516
+ description="Future Framework CLI",
517
+ formatter_class=argparse.RawDescriptionHelpFormatter,
518
+ epilog="""
519
+ examples:
520
+ future init .
521
+ future run
522
+ future routes
523
+ future make:model Trade
524
+ future make:controller Trade
525
+ future make:migration Trade
526
+ future make:migrations
527
+ future make:seed Trade
528
+ future make:seeds
529
+ future migrate
530
+ future migrate rollback
531
+ future seed
532
+ future seed TradeSeeder
533
+ """,
534
+ )
535
+ subparsers = parser.add_subparsers(dest="command")
536
+
537
+ subparsers.add_parser("routes", help="List all routes from the app in run.py")
538
+
539
+ init_parser = subparsers.add_parser("init", help="Scaffold a new Future project (app/, database/, run.py)")
540
+ init_parser.add_argument("target", nargs="?", default=".", help="Target directory (default: current directory)")
541
+
542
+ run_parser = subparsers.add_parser("run", help="Run the app from run.py")
543
+ run_parser.add_argument("--host", default=None, help="Bind host (default: app config or 127.0.0.1)")
544
+ run_parser.add_argument("--port", type=int, default=None, help="Bind port (default: app config or 8000)")
545
+ run_parser.add_argument("--workers", type=int, default=None, help="Worker processes (default: app config or 1)")
546
+
547
+ migrate_parser = subparsers.add_parser("migrate", help="Run or rollback migrations in database/migrations/")
548
+ migrate_parser.add_argument("action", nargs="?", default="run", choices=["run", "rollback"], help="run (default) or rollback last batch")
549
+
550
+ seed_parser = subparsers.add_parser("seed", help="Run seeders in database/seeds/ (loads run.py so Future registers DATABASES)")
551
+ seed_parser.add_argument("seeder", nargs="?", default=None, help="Seeder class name, or omit to run all seeders")
552
+
553
+ make_migration_parser = subparsers.add_parser("make:migration", help="Generate a Schema migration from one Model")
554
+ make_migration_parser.add_argument("model", help="Model class name")
555
+
556
+ subparsers.add_parser("make:migrations", help="Generate Schema migrations for all annotated models")
557
+
558
+ make_seed_parser = subparsers.add_parser("make:seed", help="Generate a seeder from one Model")
559
+ make_seed_parser.add_argument("model", help="Model class name")
560
+
561
+ subparsers.add_parser("make:seeds", help="Generate seeders for all annotated models (skips existing files)")
562
+
563
+ stub_help = {
564
+ "model": "Create app/models/<Name>.py",
565
+ "controller": "Create app/controllers/<Name>Controller.py",
566
+ "middleware": "Create app/middleware/<Name>Middleware.py",
567
+ "plugin": "Create app/plugins/<Name>Plugin.py",
568
+ "task": "Create app/tasks/<Name>.py",
569
+ }
570
+ for kind, help_text in stub_help.items():
571
+ stub_parser = subparsers.add_parser(f"make:{kind}", help=help_text)
572
+ stub_parser.add_argument("name", help=f"{kind.capitalize()} name")
573
+
574
+ args = parser.parse_args()
575
+
576
+ if args.command == "routes":
577
+ print_routes()
578
+ elif args.command == "init":
579
+ if args.target == ".":
580
+ scaffold_project(os.getcwd())
581
+ else:
582
+ scaffold_project(os.path.join(os.getcwd(), args.target))
583
+ elif args.command == "run":
584
+ run_app(host=args.host, port=args.port, workers=args.workers)
585
+ elif args.command == "migrate":
586
+ run_migrations(rollback=(args.action == "rollback"))
587
+ elif args.command == "seed":
588
+ run_seeds(seeder=args.seeder)
589
+ elif args.command == "make:migration":
590
+ make_migration(model_name=args.model)
591
+ elif args.command == "make:migrations":
592
+ make_migration(model_name=None)
593
+ elif args.command == "make:seed":
594
+ make_seed(model_name=args.model)
595
+ elif args.command == "make:seeds":
596
+ make_seed(model_name=None)
597
+ elif args.command and args.command.startswith("make:"):
598
+ kind = args.command.split(":", 1)[1]
599
+ if kind in stub_help:
600
+ make_stub(kind=kind, name=args.name)
601
+ else:
602
+ parser.print_help()
603
+ else:
604
+ parser.print_help()
605
+
606
+
607
+ if __name__ == "__main__":
608
+ main()