future-framework 3.1.2__tar.gz → 3.2.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 (101) hide show
  1. {future_framework-3.1.2 → future_framework-3.2.0}/PKG-INFO +2 -3
  2. {future_framework-3.1.2 → future_framework-3.2.0}/README.md +1 -2
  3. {future_framework-3.1.2 → future_framework-3.2.0}/future/application.py +52 -17
  4. future_framework-3.2.0/future/authentication/Auth0Authentication.py +15 -0
  5. future_framework-3.2.0/future/authentication/AuthenticationComponent.py +78 -0
  6. future_framework-3.2.0/future/authentication/AzureADAuthentication.py +15 -0
  7. future_framework-3.2.0/future/authentication/BasicAuthentication.py +48 -0
  8. future_framework-3.2.0/future/authentication/Identity.py +34 -0
  9. future_framework-3.2.0/future/authentication/KerberosAuthentication.py +15 -0
  10. future_framework-3.2.0/future/authentication/KeycloakAuthentication.py +15 -0
  11. future_framework-3.2.0/future/authentication/OAuth2Authentication.py +15 -0
  12. future_framework-3.2.0/future/authentication/OpenIdConnectAuthentication.py +15 -0
  13. future_framework-3.2.0/future/authentication/SAMLAuthentication.py +15 -0
  14. future_framework-3.2.0/future/authentication/__init__.py +3 -0
  15. {future_framework-3.1.2 → future_framework-3.2.0}/future/cli.py +49 -3
  16. future_framework-3.2.0/future/container.py +180 -0
  17. future_framework-3.2.0/future/interfaces/IAuthentication.py +11 -0
  18. future_framework-3.2.0/future/interfaces/IMiddleware.py +49 -0
  19. future_framework-3.2.0/future/middleware/AuthenticationMiddleware.py +27 -0
  20. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/ScopeValidationMiddleware.py +5 -6
  21. future_framework-3.2.0/future/middleware/__init__.py +7 -0
  22. {future_framework-3.1.2 → future_framework-3.2.0}/future/request.py +7 -1
  23. {future_framework-3.1.2 → future_framework-3.2.0}/future/routing.py +10 -10
  24. {future_framework-3.1.2 → future_framework-3.2.0}/future/types.py +1 -1
  25. {future_framework-3.1.2 → future_framework-3.2.0}/pyproject.toml +1 -1
  26. {future_framework-3.1.2 → future_framework-3.2.0}/pyproject.toml.orig +1 -1
  27. future_framework-3.1.2/future/authentication/Auth0Authentication.py +0 -9
  28. future_framework-3.1.2/future/authentication/AzureADAuthentication.py +0 -9
  29. future_framework-3.1.2/future/authentication/BasicAuthentication.py +0 -9
  30. future_framework-3.1.2/future/authentication/KerberosAuthentication.py +0 -9
  31. future_framework-3.1.2/future/authentication/KeycloakAuthentication.py +0 -9
  32. future_framework-3.1.2/future/authentication/OAuth2Authentication.py +0 -9
  33. future_framework-3.1.2/future/authentication/OpenIdConnectAuthentication.py +0 -9
  34. future_framework-3.1.2/future/authentication/SAMLAuthentication.py +0 -9
  35. future_framework-3.1.2/future/interfaces/IAuthentication.py +0 -5
  36. future_framework-3.1.2/future/interfaces/IMiddleware.py +0 -26
  37. future_framework-3.1.2/future/plugins/__init__.py +0 -0
  38. future_framework-3.1.2/future/tasks/__init__.py +0 -0
  39. {future_framework-3.1.2 → future_framework-3.2.0}/LICENSE +0 -0
  40. {future_framework-3.1.2 → future_framework-3.2.0}/future/__init__.py +0 -0
  41. {future_framework-3.1.2 → future_framework-3.2.0}/future/controllers/DebugController.py +0 -0
  42. {future_framework-3.1.2 → future_framework-3.2.0}/future/controllers/GraphQLController.py +0 -0
  43. {future_framework-3.1.2 → future_framework-3.2.0}/future/controllers/OpenAPIController.py +0 -0
  44. {future_framework-3.1.2 → future_framework-3.2.0}/future/controllers/WebSocketController.py +0 -0
  45. {future_framework-3.1.2 → future_framework-3.2.0}/future/controllers/WelcomeController.py +0 -0
  46. {future_framework-3.1.2/future/authentication → future_framework-3.2.0/future/controllers}/__init__.py +0 -0
  47. {future_framework-3.1.2 → future_framework-3.2.0}/future/database.py +0 -0
  48. {future_framework-3.1.2 → future_framework-3.2.0}/future/databases/ClickhouseDatabase.py +0 -0
  49. {future_framework-3.1.2 → future_framework-3.2.0}/future/databases/ElasticsearchDatabase.py +0 -0
  50. {future_framework-3.1.2 → future_framework-3.2.0}/future/databases/MongoDBDatabase.py +0 -0
  51. {future_framework-3.1.2 → future_framework-3.2.0}/future/databases/MySQLDatabase.py +0 -0
  52. {future_framework-3.1.2 → future_framework-3.2.0}/future/databases/PostgresDatabase.py +0 -0
  53. {future_framework-3.1.2 → future_framework-3.2.0}/future/databases/RedisDatabase.py +0 -0
  54. {future_framework-3.1.2 → future_framework-3.2.0}/future/databases/SQLiteDatabase.py +0 -0
  55. {future_framework-3.1.2/future/controllers → future_framework-3.2.0/future/databases}/__init__.py +0 -0
  56. {future_framework-3.1.2 → future_framework-3.2.0}/future/exceptions.py +0 -0
  57. {future_framework-3.1.2 → future_framework-3.2.0}/future/interfaces/IController.py +0 -0
  58. {future_framework-3.1.2 → future_framework-3.2.0}/future/interfaces/IDatabase.py +0 -0
  59. {future_framework-3.1.2 → future_framework-3.2.0}/future/interfaces/IModel.py +0 -0
  60. {future_framework-3.1.2 → future_framework-3.2.0}/future/interfaces/IPlugin.py +0 -0
  61. {future_framework-3.1.2 → future_framework-3.2.0}/future/interfaces/ITask.py +0 -0
  62. {future_framework-3.1.2/future/databases → future_framework-3.2.0/future/interfaces}/__init__.py +0 -0
  63. {future_framework-3.1.2 → future_framework-3.2.0}/future/interfacing.py +0 -0
  64. {future_framework-3.1.2 → future_framework-3.2.0}/future/lifespan.py +0 -0
  65. {future_framework-3.1.2 → future_framework-3.2.0}/future/logger.py +0 -0
  66. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/BruteforcePreventionMiddleware.py +0 -0
  67. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/CORSMiddleware.py +0 -0
  68. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/CSRFMiddleware.py +0 -0
  69. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/GZipMiddleware.py +0 -0
  70. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/HTAccessConfuserMiddleware.py +0 -0
  71. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/HeaderConfuserMiddleware.py +0 -0
  72. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/RateLimitMiddleware.py +0 -0
  73. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/ResponseCodeConfuserMiddleware.py +0 -0
  74. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/SQLiConfuserMiddleware.py +0 -0
  75. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/SessionMiddleware.py +0 -0
  76. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/TestMiddlewareRequest.py +0 -0
  77. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/TestMiddlewareResponse.py +0 -0
  78. {future_framework-3.1.2 → future_framework-3.2.0}/future/middleware/WebServerConfuserMiddleware.py +0 -0
  79. {future_framework-3.1.2 → future_framework-3.2.0}/future/migrations.py +0 -0
  80. {future_framework-3.1.2 → future_framework-3.2.0}/future/models/PostModel.py +0 -0
  81. {future_framework-3.1.2 → future_framework-3.2.0}/future/models/Query.py +0 -0
  82. {future_framework-3.1.2 → future_framework-3.2.0}/future/models/UserModel.py +0 -0
  83. {future_framework-3.1.2/future/interfaces → future_framework-3.2.0/future/models}/__init__.py +0 -0
  84. {future_framework-3.1.2 → future_framework-3.2.0}/future/openapi.py +0 -0
  85. {future_framework-3.1.2 → future_framework-3.2.0}/future/plugins/ElasticsearchPlugin.py +0 -0
  86. {future_framework-3.1.2/future/middleware → future_framework-3.2.0/future/plugins}/__init__.py +0 -0
  87. {future_framework-3.1.2 → future_framework-3.2.0}/future/response.py +0 -0
  88. {future_framework-3.1.2 → future_framework-3.2.0}/future/scheduling.py +0 -0
  89. {future_framework-3.1.2 → future_framework-3.2.0}/future/seeder.py +0 -0
  90. {future_framework-3.1.2 → future_framework-3.2.0}/future/settings.py +0 -0
  91. {future_framework-3.1.2 → future_framework-3.2.0}/future/tasks/CheckDNSTask.py +0 -0
  92. {future_framework-3.1.2 → future_framework-3.2.0}/future/tasks/CheckDiskUsageTask.py +0 -0
  93. {future_framework-3.1.2 → future_framework-3.2.0}/future/tasks/CheckHttpStatusTask.py +0 -0
  94. {future_framework-3.1.2 → future_framework-3.2.0}/future/tasks/CheckMemoryUsageTask.py +0 -0
  95. {future_framework-3.1.2 → future_framework-3.2.0}/future/tasks/CheckSSHBannerTask.py +0 -0
  96. {future_framework-3.1.2 → future_framework-3.2.0}/future/tasks/CheckSystemUptimeTask.py +0 -0
  97. {future_framework-3.1.2 → future_framework-3.2.0}/future/tasks/DailyBackupTask.py +0 -0
  98. {future_framework-3.1.2 → future_framework-3.2.0}/future/tasks/SyncTaskExample.py +0 -0
  99. {future_framework-3.1.2/future/models → future_framework-3.2.0/future/tasks}/__init__.py +0 -0
  100. {future_framework-3.1.2 → future_framework-3.2.0}/future/taskscheduler.py +0 -0
  101. {future_framework-3.1.2 → future_framework-3.2.0}/future/testclient.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: future-framework
3
- Version: 3.1.2
3
+ Version: 3.2.0
4
4
  Summary: ASGI-based framework for web APIs
5
5
  Author: nicolaipre
6
6
  License-Expression: MIT
@@ -50,7 +50,6 @@ Import name is `future` (`from future.application import Future`).
50
50
  ```python
51
51
  from future.application import Future
52
52
  from future.interfaces.IController import IController
53
- from future.lifespan import Lifespan
54
53
  from future.response import Response
55
54
  from future.routing import Get, RouteGroup
56
55
 
@@ -58,7 +57,7 @@ class HomeController(IController):
58
57
  async def index(self) -> Response:
59
58
  return self.response.json({"ok": True})
60
59
 
61
- app = Future(lifespan=Lifespan([], [], []), config={"APP_DOMAIN": "", "APP_NAME": "Demo"})
60
+ app = Future(config={"APP_DOMAIN": "", "APP_NAME": "Demo"})
62
61
  app.add_routes([RouteGroup(name="Main", routes=[Get("/", HomeController.index, "home")])])
63
62
  app.run(host="127.0.0.1", port=8000)
64
63
  ```
@@ -20,7 +20,6 @@ Import name is `future` (`from future.application import Future`).
20
20
  ```python
21
21
  from future.application import Future
22
22
  from future.interfaces.IController import IController
23
- from future.lifespan import Lifespan
24
23
  from future.response import Response
25
24
  from future.routing import Get, RouteGroup
26
25
 
@@ -28,7 +27,7 @@ class HomeController(IController):
28
27
  async def index(self) -> Response:
29
28
  return self.response.json({"ok": True})
30
29
 
31
- app = Future(lifespan=Lifespan([], [], []), config={"APP_DOMAIN": "", "APP_NAME": "Demo"})
30
+ app = Future(config={"APP_DOMAIN": "", "APP_NAME": "Demo"})
32
31
  app.add_routes([RouteGroup(name="Main", routes=[Get("/", HomeController.index, "home")])])
33
32
  app.run(host="127.0.0.1", port=8000)
34
33
  ```
@@ -1,6 +1,7 @@
1
1
  import platform
2
2
  import sys
3
3
  import logging
4
+ import inspect
4
5
  import traceback
5
6
 
6
7
  from collections.abc import Sequence
@@ -16,6 +17,8 @@ from rich.table import Table
16
17
  from rich.text import Text
17
18
 
18
19
  from future.exceptions import ErrorHandler, HTTPException
20
+ from future.authentication.AuthenticationComponent import AuthenticationComponent
21
+ from future.container import Container
19
22
  from future.logger import create_uvicorn_logging_config, log
20
23
  from future.interfaces.IMiddleware import IMiddleware
21
24
  from future.openapi import get_openapi_config, is_docs_path, openapi_routes, rebuild_spec_from_routes, set_openapi_config, spec_path
@@ -72,16 +75,28 @@ while True:
72
75
 
73
76
 
74
77
  class Future:
75
- def __init__(self, lifespan: Any, config: dict[str, Any] | None = None) -> None:
78
+ def __init__(self, lifespan: Any | None = None, config: dict[str, Any] | None = None) -> None:
76
79
  # spawn background tasks in the lifespan startup process... or database connections etc...
77
80
  # on shutdown, save shit, send info about shutdown etc...
81
+ if lifespan is None:
82
+ from future.lifespan import Lifespan
83
+ lifespan = Lifespan()
78
84
  self.lifespan = lifespan
79
85
  self.routes: dict[str, dict[str, RouteConfig]] = {}
80
86
  self.config = config or {}
87
+ self._container = Container(self.config)
88
+ self._container.instance(Container, self._container)
89
+ self._container.configure_bindings(self.config.get("BINDINGS"))
81
90
  self.databases = self.config.get("DATABASES")
82
91
  if self.databases:
83
92
  from future.database import Database
84
- Database().set_connection_details(self.databases)
93
+ database = Database()
94
+ database.set_connection_details(self.databases)
95
+ self._container.component(database, Database)
96
+
97
+ authentication = self.config.get("AUTHENTICATION")
98
+ if authentication:
99
+ self._container.component(AuthenticationComponent(self._container, authentication), AuthenticationComponent)
85
100
 
86
101
  domain = self.config.get("APP_DOMAIN", "")
87
102
 
@@ -117,9 +132,26 @@ class Future:
117
132
 
118
133
  def set_config(self, config: dict[str, Any]) -> None:
119
134
  self.config = config
135
+ self._container.config = config
120
136
  set_openapi_config(self.config)
121
137
  log.setLevel(logging.DEBUG if bool(self.config.get("APP_DEBUG")) else logging.INFO)
122
138
 
139
+ def override(self, dependency: Any, value: Any) -> None:
140
+ """Replace a resolved dependency, primarily for application tests."""
141
+ self._container.override(dependency, value)
142
+
143
+ def _middleware_priority(self, middleware: Any) -> int:
144
+ return int(getattr(middleware, "priority", 0))
145
+
146
+ def _bind_middleware(self, definition: Any, request: Request, response: Response) -> IMiddleware:
147
+ if inspect.isclass(definition):
148
+ middleware = self._container.resolve(definition, provided={Request: request, Response: response})
149
+ else:
150
+ middleware = definition
151
+ if not isinstance(middleware, IMiddleware):
152
+ raise TypeError(f"Middleware must extend IMiddleware, got {type(middleware).__name__}")
153
+ return middleware.bind(request, response, self._container)
154
+
123
155
  def _resolve_controller_action(self, endpoint: Any) -> tuple[Any, str | None]:
124
156
  qual = getattr(endpoint, "__qualname__", "") or ""
125
157
  mod_name = getattr(endpoint, "__module__", None)
@@ -149,8 +181,8 @@ class Future:
149
181
 
150
182
  middleware_classes: list[Any] = []
151
183
  if parent_middlewares:
152
- middleware_classes.extend(sorted(parent_middlewares, key=lambda m: m.priority))
153
- middleware_classes.extend(sorted(route.middlewares, key=lambda m: m.priority))
184
+ middleware_classes.extend(sorted(parent_middlewares, key=self._middleware_priority))
185
+ middleware_classes.extend(sorted(route.middlewares, key=self._middleware_priority))
154
186
 
155
187
  controller_cls, action = self._resolve_controller_action(route.endpoint)
156
188
 
@@ -378,15 +410,18 @@ class Future:
378
410
 
379
411
  started = False
380
412
  try:
381
- # async with self.lifespan(app) as state:
382
- self.lifespan.app = app
383
- async with self.lifespan as state:
384
- if state is not None:
385
- scope.setdefault("state", {}).update(state)
386
- await send({"type": AsgiEventType.LIFESPAN_STARTUP_COMPLETE})
387
- started = True
388
- message = await receive()
389
- assert message["type"] == AsgiEventType.LIFESPAN_SHUTDOWN
413
+ await self._container.start()
414
+ try:
415
+ self.lifespan.app = app
416
+ async with self.lifespan as state:
417
+ if state is not None:
418
+ scope.setdefault("state", {}).update(state)
419
+ await send({"type": AsgiEventType.LIFESPAN_STARTUP_COMPLETE})
420
+ started = True
421
+ message = await receive()
422
+ assert message["type"] == AsgiEventType.LIFESPAN_SHUTDOWN
423
+ finally:
424
+ await self._container.stop()
390
425
 
391
426
  except BaseException:
392
427
  event_type = AsgiEventType.LIFESPAN_SHUTDOWN_FAILED if started else AsgiEventType.LIFESPAN_STARTUP_FAILED
@@ -446,7 +481,7 @@ class Future:
446
481
 
447
482
  request.route = matched_route.get("route")
448
483
  middleware_classes = matched_route["middleware"].get("classes") or []
449
- middleware_instances = [m(request, response) for m in middleware_classes]
484
+ middleware_instances = [self._bind_middleware(middleware, request, response) for middleware in middleware_classes]
450
485
  for mw in middleware_instances:
451
486
  early = await mw.before()
452
487
  if early is not None:
@@ -456,7 +491,7 @@ class Future:
456
491
  controller_cls = matched_route.get("controller")
457
492
  action = matched_route.get("action")
458
493
  if controller_cls and action:
459
- ctrl = controller_cls(request, response)
494
+ ctrl = self._container.resolve(controller_cls, provided={Request: request, Response: response})
460
495
  method = getattr(ctrl, action)
461
496
  if route_params:
462
497
  result = await method(**route_params)
@@ -527,7 +562,7 @@ class Future:
527
562
  request.route = matched_route.get("route")
528
563
  try:
529
564
  middleware_classes = matched_route["middleware"].get("classes") or []
530
- middleware_instances = [m(request, response) for m in middleware_classes]
565
+ middleware_instances = [self._bind_middleware(middleware, request, response) for middleware in middleware_classes]
531
566
  for mw in middleware_instances:
532
567
  early = await mw.before()
533
568
  if early is not None:
@@ -537,7 +572,7 @@ class Future:
537
572
  controller_cls = matched_route.get("controller")
538
573
  action = matched_route.get("action")
539
574
  if controller_cls and action:
540
- ctrl = controller_cls(request, response)
575
+ ctrl = self._container.resolve(controller_cls, provided={Request: request, Response: response})
541
576
  method = getattr(ctrl, action)
542
577
  if route_params:
543
578
  result = await method(**route_params)
@@ -0,0 +1,15 @@
1
+ """
2
+ Auth0 authentication.
3
+ """
4
+
5
+ from future.interfaces.IAuthentication import IAuthentication
6
+ from future.authentication.Identity import Identity
7
+ from future.request import Request
8
+
9
+
10
+ class Auth0Authentication(IAuthentication):
11
+ auth_type = "oauth"
12
+
13
+ async def authenticate(self, request: Request) -> Identity | None:
14
+ message = "Auth0 authentication is not implemented yet"
15
+ raise NotImplementedError(message)
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+
5
+ from typing import Any
6
+
7
+ from future.authentication.Identity import Identity
8
+ from future.container import Container
9
+ from future.request import Request
10
+
11
+
12
+ class AuthenticationComponent:
13
+ """Registry and lifecycle owner for named authentication providers."""
14
+
15
+ def __init__(self, container: Container, config: dict[str, Any] | None = None) -> None:
16
+ self._container = container
17
+ self.config = config or {}
18
+ self.default = self.config.get("default")
19
+ self.providers: dict[str, Any] = {}
20
+ for name, definition in self.config.get("providers", {}).items():
21
+ self.register(name, self._build_provider(name, definition))
22
+ if self.default is None and len(self.providers) == 1:
23
+ self.default = next(iter(self.providers))
24
+
25
+ def register(self, name: str, provider: Any) -> None:
26
+ if not name:
27
+ raise ValueError("Authentication provider names cannot be empty")
28
+ if not callable(getattr(provider, "authenticate", None)):
29
+ raise TypeError(f"Authentication provider {name!r} must define authenticate(request)")
30
+ self.providers[name] = provider
31
+
32
+ def get(self, name: str | None = None) -> Any:
33
+ selected = name or self.default
34
+ if not selected:
35
+ raise LookupError("No default authentication provider is configured")
36
+ try:
37
+ return self.providers[selected]
38
+ except KeyError as exc:
39
+ raise LookupError(f"Authentication provider {selected!r} is not configured") from exc
40
+
41
+ async def authenticate(self, request: Request, provider: str | None = None) -> Identity | None:
42
+ selected = provider or self.default
43
+ authenticator = self.get(selected)
44
+ result = authenticator.authenticate(request)
45
+ if inspect.isawaitable(result):
46
+ result = await result
47
+ if result is not None and not isinstance(result, Identity):
48
+ raise TypeError(f"Authentication provider {selected!r} must return Identity or None, got {type(result).__name__}")
49
+ return result
50
+
51
+ async def start(self) -> None:
52
+ for provider in self.providers.values():
53
+ start = getattr(provider, "start", None)
54
+ if start is not None:
55
+ result = start()
56
+ if inspect.isawaitable(result):
57
+ await result
58
+
59
+ async def stop(self) -> None:
60
+ for provider in reversed(list(self.providers.values())):
61
+ stop = getattr(provider, "stop", None)
62
+ if stop is not None:
63
+ result = stop()
64
+ if inspect.isawaitable(result):
65
+ await result
66
+
67
+ def _build_provider(self, name: str, definition: Any) -> Any:
68
+ if not isinstance(definition, dict):
69
+ if inspect.isclass(definition):
70
+ return self._container.resolve(definition)
71
+ return definition
72
+ driver = definition.get("driver")
73
+ if driver is None:
74
+ raise ValueError(f"Authentication provider {name!r} is missing its driver")
75
+ options = definition.get("options") or {}
76
+ if not isinstance(options, dict):
77
+ raise TypeError(f"Authentication provider {name!r} options must be a dictionary")
78
+ return self._container.resolve(driver, options=options)
@@ -0,0 +1,15 @@
1
+ """
2
+ Azure AD authentication.
3
+ """
4
+
5
+ from future.interfaces.IAuthentication import IAuthentication
6
+ from future.authentication.Identity import Identity
7
+ from future.request import Request
8
+
9
+
10
+ class AzureADAuthentication(IAuthentication):
11
+ auth_type = "basic"
12
+
13
+ async def authenticate(self, request: Request) -> Identity | None:
14
+ message = "Azure AD authentication is not implemented yet"
15
+ raise NotImplementedError(message)
@@ -0,0 +1,48 @@
1
+ """HTTP Basic authentication backed by an application credential verifier."""
2
+
3
+ import base64
4
+ import binascii
5
+ import inspect
6
+
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+ from future.authentication.Identity import Identity
11
+ from future.interfaces.IAuthentication import IAuthentication
12
+ from future.request import Request
13
+
14
+
15
+ class BasicAuthentication(IAuthentication):
16
+ auth_type = "basic"
17
+
18
+ def __init__(self, verify_credentials: Callable[[str, str], Any], realm: str = "Future") -> None:
19
+ self.verify_credentials = verify_credentials
20
+ self.challenge = f'Basic realm="{realm}"'
21
+
22
+ async def authenticate(self, request: Request) -> Identity | None:
23
+ authorization = request.headers.get("authorization", "")
24
+ scheme, _, encoded = authorization.partition(" ")
25
+ if scheme.lower() != "basic" or not encoded:
26
+ return None
27
+ try:
28
+ username, password = base64.b64decode(encoded, validate=True).decode("utf-8").split(":", 1)
29
+ except (binascii.Error, ValueError, UnicodeDecodeError):
30
+ return None
31
+ result = self.verify_credentials(username, password)
32
+ if inspect.isawaitable(result):
33
+ result = await result
34
+ if result is None or result is False:
35
+ return None
36
+ if isinstance(result, Identity):
37
+ return result
38
+ if result is True:
39
+ return Identity(subject=username, provider="basic")
40
+ if isinstance(result, str):
41
+ return Identity(subject=result, provider="basic")
42
+ if isinstance(result, dict):
43
+ claims = dict(result.get("claims") or {})
44
+ subject = str(result.get("subject") or result.get("id") or username)
45
+ roles = tuple(str(role) for role in result.get("roles", ()))
46
+ scopes = tuple(str(scope) for scope in result.get("scopes", ()))
47
+ return Identity(subject=subject, provider="basic", claims=claims, roles=roles, scopes=scopes)
48
+ raise TypeError(f"Basic credential verifier must return Identity, dict, str, bool, or None, got {type(result).__name__}")
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class Identity:
7
+ __slots__ = ("subject", "provider", "claims", "roles", "scopes", "_initialized")
8
+ subject: str
9
+ provider: str
10
+ claims: dict[str, Any]
11
+ roles: tuple[str, ...]
12
+ scopes: tuple[str, ...]
13
+ _initialized: bool
14
+
15
+ def __init__(self, subject: str, provider: str, claims: dict[str, Any] | None = None, roles: tuple[str, ...] = (), scopes: tuple[str, ...] = ()) -> None:
16
+ object.__setattr__(self, "subject", subject)
17
+ object.__setattr__(self, "provider", provider)
18
+ object.__setattr__(self, "claims", dict(claims or {}))
19
+ object.__setattr__(self, "roles", tuple(roles))
20
+ object.__setattr__(self, "scopes", tuple(scopes))
21
+ object.__setattr__(self, "_initialized", True)
22
+
23
+ def __setattr__(self, name: str, value: Any) -> None:
24
+ if getattr(self, "_initialized", False):
25
+ raise AttributeError("Identity is immutable")
26
+ object.__setattr__(self, name, value)
27
+
28
+ def __eq__(self, other: object) -> bool:
29
+ if not isinstance(other, Identity):
30
+ return False
31
+ return (self.subject, self.provider, self.claims, self.roles, self.scopes) == (other.subject, other.provider, other.claims, other.roles, other.scopes)
32
+
33
+ def __repr__(self) -> str:
34
+ return f"Identity(subject={self.subject!r}, provider={self.provider!r}, claims={self.claims!r}, roles={self.roles!r}, scopes={self.scopes!r})"
@@ -0,0 +1,15 @@
1
+ """
2
+ Kerberos Single Sign-On authentication.
3
+ """
4
+
5
+ from future.interfaces.IAuthentication import IAuthentication
6
+ from future.authentication.Identity import Identity
7
+ from future.request import Request
8
+
9
+
10
+ class KerberosAuthentication(IAuthentication):
11
+ auth_type = "sso"
12
+
13
+ async def authenticate(self, request: Request) -> Identity | None:
14
+ message = "Kerberos authentication is not implemented yet"
15
+ raise NotImplementedError(message)
@@ -0,0 +1,15 @@
1
+ """
2
+ Keycloak authentication.
3
+ """
4
+
5
+ from future.interfaces.IAuthentication import IAuthentication
6
+ from future.authentication.Identity import Identity
7
+ from future.request import Request
8
+
9
+
10
+ class KeycloakAuthentication(IAuthentication):
11
+ auth_type = "oauth"
12
+
13
+ async def authenticate(self, request: Request) -> Identity | None:
14
+ message = "Keycloak authentication is not implemented yet"
15
+ raise NotImplementedError(message)
@@ -0,0 +1,15 @@
1
+ """
2
+ OAuth 2 authentication.
3
+ """
4
+
5
+ from future.interfaces.IAuthentication import IAuthentication
6
+ from future.authentication.Identity import Identity
7
+ from future.request import Request
8
+
9
+
10
+ class OAuth2Authentication(IAuthentication):
11
+ auth_type = "oauth"
12
+
13
+ async def authenticate(self, request: Request) -> Identity | None:
14
+ message = "OAuth 2 authentication is not implemented yet"
15
+ raise NotImplementedError(message)
@@ -0,0 +1,15 @@
1
+ """
2
+ OpenID Connect authentication.
3
+ """
4
+
5
+ from future.interfaces.IAuthentication import IAuthentication
6
+ from future.authentication.Identity import Identity
7
+ from future.request import Request
8
+
9
+
10
+ class OpenIdConnectAuthentication(IAuthentication):
11
+ auth_type = "oidc"
12
+
13
+ async def authenticate(self, request: Request) -> Identity | None:
14
+ message = "OpenID Connect authentication is not implemented yet"
15
+ raise NotImplementedError(message)
@@ -0,0 +1,15 @@
1
+ """
2
+ SAML Single Sign-On authentication.
3
+ """
4
+
5
+ from future.interfaces.IAuthentication import IAuthentication
6
+ from future.authentication.Identity import Identity
7
+ from future.request import Request
8
+
9
+
10
+ class SAMLAuthentication(IAuthentication):
11
+ auth_type = "sso"
12
+
13
+ async def authenticate(self, request: Request) -> Identity | None:
14
+ message = "SAML authentication is not implemented yet"
15
+ raise NotImplementedError(message)
@@ -0,0 +1,3 @@
1
+ from future.authentication.AuthenticationComponent import AuthenticationComponent
2
+ from future.authentication.BasicAuthentication import BasicAuthentication
3
+ from future.authentication.Identity import Identity
@@ -11,7 +11,7 @@ from typing import Any
11
11
 
12
12
 
13
13
  APP_ROOT = "app"
14
- APP_DIRS = ["config", "controllers", "middleware", "models", "plugins", "tasks"]
14
+ APP_DIRS = ["authentication", "config", "controllers", "interfaces", "middleware", "models", "plugins", "repositories", "services", "tasks"]
15
15
 
16
16
  STUB_KINDS = {
17
17
  "readme": {
@@ -172,7 +172,7 @@ STUB_KINDS = {
172
172
  "template": dedent("""
173
173
  from app.controllers.ExampleController import ExampleController
174
174
  from app.middleware.ExampleMiddleware import ExampleMiddleware
175
- from future.openapi import openapi_routes
175
+ from future.controllers.OpenAPIController import OpenAPIController
176
176
  from future.routing import RouteGroup, Get
177
177
 
178
178
 
@@ -181,7 +181,14 @@ STUB_KINDS = {
181
181
  name="Docs",
182
182
  prefix="/api",
183
183
  # middlewares=[ExampleMiddleware], # protect docs if needed
184
- routes=openapi_routes(uis=["swagger", "redoc", "scalar", "rapidoc"]),
184
+ routes=[
185
+ Get("/openapi.json", OpenAPIController.openapi, "openapi"),
186
+ Get("/swagger-config", OpenAPIController.swagger_config, "swagger-config"),
187
+ Get("/docs", OpenAPIController.swagger, "docs"),
188
+ Get("/redoc", OpenAPIController.redoc, "redoc"),
189
+ Get("/scalar", OpenAPIController.scalar, "scalar"),
190
+ Get("/rapidoc", OpenAPIController.rapidoc, "rapidoc"),
191
+ ],
185
192
  ),
186
193
  RouteGroup(
187
194
  name="Main",
@@ -332,6 +339,36 @@ STUB_KINDS = {
332
339
  """).lstrip("\n"),
333
340
  "init": {"class_name": "ExampleMiddleware"},
334
341
  },
342
+ "authentication": {
343
+ "directory": "app/authentication",
344
+ "suffix": "Authentication",
345
+ "template": dedent("""
346
+ from future.authentication.Identity import Identity
347
+ from future.interfaces.IAuthentication import IAuthentication
348
+ from future.request import Request
349
+
350
+
351
+ class {class_name}(IAuthentication):
352
+ async def authenticate(self, request: Request) -> Identity | None:
353
+ return None
354
+ """).lstrip("\n"),
355
+ },
356
+ "repository": {
357
+ "directory": "app/repositories",
358
+ "suffix": "Repository",
359
+ "template": dedent("""
360
+ class {class_name}:
361
+ pass
362
+ """).lstrip("\n"),
363
+ },
364
+ "service": {
365
+ "directory": "app/services",
366
+ "suffix": "Service",
367
+ "template": dedent("""
368
+ class {class_name}:
369
+ pass
370
+ """).lstrip("\n"),
371
+ },
335
372
  "plugin": {
336
373
  "directory": "app/plugins",
337
374
  "suffix": "Plugin",
@@ -399,6 +436,9 @@ class StubMaker:
399
436
  def make(self, kind, name):
400
437
  if kind not in STUB_KINDS:
401
438
  raise ValueError(f"Unknown stub kind: {kind}")
439
+ is_interface_name = name.startswith("I") and len(name) > 1 and name[1].isupper()
440
+ if is_interface_name:
441
+ raise ValueError("The I prefix is reserved for interfaces in app/interfaces")
402
442
  config = STUB_KINDS[kind]
403
443
  if "directory" not in config:
404
444
  raise ValueError(f"Stub kind {kind!r} is not a make:* target")
@@ -651,6 +691,9 @@ examples:
651
691
  future routes
652
692
  future make:model Trade
653
693
  future make:controller Trade
694
+ future make:service Trade
695
+ future make:repository Trade
696
+ future make:authentication LDAP
654
697
  future make:migration Trade
655
698
  future make:migrations
656
699
  future make:seed Trade
@@ -690,10 +733,13 @@ examples:
690
733
  subparsers.add_parser("make:seeds", help="Generate seeders for all annotated models (skips existing files)")
691
734
 
692
735
  stub_help = {
736
+ "authentication": "Create app/authentication/<Name>Authentication.py",
693
737
  "model": "Create app/models/<Name>.py",
694
738
  "controller": "Create app/controllers/<Name>Controller.py",
695
739
  "middleware": "Create app/middleware/<Name>Middleware.py",
696
740
  "plugin": "Create app/plugins/<Name>Plugin.py",
741
+ "repository": "Create app/repositories/<Name>Repository.py",
742
+ "service": "Create app/services/<Name>Service.py",
697
743
  "task": "Create app/tasks/<Name>.py",
698
744
  }
699
745
  for kind, help_text in stub_help.items():