bazimya 0.2.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 (193) hide show
  1. bazimya/__init__.py +146 -0
  2. bazimya/__main__.py +8 -0
  3. bazimya/auth/__init__.py +12 -0
  4. bazimya/auth/guard.py +251 -0
  5. bazimya/auth/middleware.py +76 -0
  6. bazimya/cache/__init__.py +5 -0
  7. bazimya/cache/repository.py +206 -0
  8. bazimya/console/__init__.py +7 -0
  9. bazimya/console/command.py +164 -0
  10. bazimya/console/commands/__init__.py +1 -0
  11. bazimya/console/commands/build.py +60 -0
  12. bazimya/console/commands/doctor.py +311 -0
  13. bazimya/console/commands/inspect.py +302 -0
  14. bazimya/console/commands/make.py +451 -0
  15. bazimya/console/commands/migrate.py +210 -0
  16. bazimya/console/commands/new.py +176 -0
  17. bazimya/console/commands/serve.py +62 -0
  18. bazimya/console/kernel.py +408 -0
  19. bazimya/console/output.py +143 -0
  20. bazimya/console/registry.py +44 -0
  21. bazimya/database/__init__.py +22 -0
  22. bazimya/database/connection.py +328 -0
  23. bazimya/database/migration.py +287 -0
  24. bazimya/database/model.py +512 -0
  25. bazimya/database/query.py +497 -0
  26. bazimya/database/schema.py +650 -0
  27. bazimya/deployment/__init__.py +5 -0
  28. bazimya/deployment/bundler.py +751 -0
  29. bazimya/events/__init__.py +5 -0
  30. bazimya/events/dispatcher.py +144 -0
  31. bazimya/extensions/__init__.py +12 -0
  32. bazimya/extensions/extension.py +198 -0
  33. bazimya/extensions/manager.py +234 -0
  34. bazimya/facades.py +267 -0
  35. bazimya/filesystem/__init__.py +5 -0
  36. bazimya/filesystem/storage.py +211 -0
  37. bazimya/foundation/__init__.py +13 -0
  38. bazimya/foundation/application.py +613 -0
  39. bazimya/foundation/container.py +121 -0
  40. bazimya/foundation/provider.py +68 -0
  41. bazimya/hashing.py +190 -0
  42. bazimya/http/__init__.py +58 -0
  43. bazimya/http/controller.py +84 -0
  44. bazimya/http/exceptions.py +100 -0
  45. bazimya/http/form_request.py +90 -0
  46. bazimya/http/middleware.py +94 -0
  47. bazimya/http/request.py +435 -0
  48. bazimya/http/response.py +232 -0
  49. bazimya/http/route.py +204 -0
  50. bazimya/http/router.py +433 -0
  51. bazimya/mail/__init__.py +5 -0
  52. bazimya/mail/mailer.py +291 -0
  53. bazimya/notifications/__init__.py +5 -0
  54. bazimya/notifications/notification.py +177 -0
  55. bazimya/serving.py +251 -0
  56. bazimya/session/__init__.py +13 -0
  57. bazimya/session/middleware.py +222 -0
  58. bazimya/session/store.py +353 -0
  59. bazimya/stubs/app/.editorconfig.stub +15 -0
  60. bazimya/stubs/app/.env.example.stub +40 -0
  61. bazimya/stubs/app/.gitattributes.stub +10 -0
  62. bazimya/stubs/app/.gitignore.stub +19 -0
  63. bazimya/stubs/app/app/Console/Commands/GreetCommand.py.stub +25 -0
  64. bazimya/stubs/app/app/Console/Commands/__init__.py.stub +24 -0
  65. bazimya/stubs/app/app/Console/Kernel.py.stub +22 -0
  66. bazimya/stubs/app/app/Console/__init__.py.stub +0 -0
  67. bazimya/stubs/app/app/Exceptions/Handler.py.stub +28 -0
  68. bazimya/stubs/app/app/Exceptions/__init__.py.stub +0 -0
  69. bazimya/stubs/app/app/Http/Controllers/AuthController.py.stub +75 -0
  70. bazimya/stubs/app/app/Http/Controllers/Controller.py.stub +11 -0
  71. bazimya/stubs/app/app/Http/Controllers/HomeController.py.stub +22 -0
  72. bazimya/stubs/app/app/Http/Controllers/__init__.py.stub +36 -0
  73. bazimya/stubs/app/app/Http/Kernel.py.stub +53 -0
  74. bazimya/stubs/app/app/Http/Middleware/Authenticate.py.stub +10 -0
  75. bazimya/stubs/app/app/Http/Middleware/EnsureEmailIsVerified.py.stub +21 -0
  76. bazimya/stubs/app/app/Http/Middleware/RedirectIfAuthenticated.py.stub +7 -0
  77. bazimya/stubs/app/app/Http/Middleware/TrustProxies.py.stub +25 -0
  78. bazimya/stubs/app/app/Http/Middleware/VerifyCsrfToken.py.stub +16 -0
  79. bazimya/stubs/app/app/Http/Middleware/__init__.py.stub +36 -0
  80. bazimya/stubs/app/app/Http/Requests/__init__.py.stub +21 -0
  81. bazimya/stubs/app/app/Http/__init__.py.stub +0 -0
  82. bazimya/stubs/app/app/Models/User.py.stub +15 -0
  83. bazimya/stubs/app/app/Models/__init__.py.stub +36 -0
  84. bazimya/stubs/app/app/Notifications/WelcomeNotification.py.stub +28 -0
  85. bazimya/stubs/app/app/Notifications/__init__.py.stub +21 -0
  86. bazimya/stubs/app/app/Providers/AppServiceProvider.py.stub +14 -0
  87. bazimya/stubs/app/app/Providers/AuthServiceProvider.py.stub +11 -0
  88. bazimya/stubs/app/app/Providers/EventServiceProvider.py.stub +15 -0
  89. bazimya/stubs/app/app/Providers/RouteServiceProvider.py.stub +19 -0
  90. bazimya/stubs/app/app/Providers/__init__.py.stub +0 -0
  91. bazimya/stubs/app/app/Rules/__init__.py.stub +0 -0
  92. bazimya/stubs/app/app/Services/__init__.py.stub +0 -0
  93. bazimya/stubs/app/app/Support/__init__.py.stub +0 -0
  94. bazimya/stubs/app/app/View/Components/Alert.py.stub +18 -0
  95. bazimya/stubs/app/app/View/Components/__init__.py.stub +22 -0
  96. bazimya/stubs/app/app/View/__init__.py.stub +0 -0
  97. bazimya/stubs/app/app/__init__.py.stub +0 -0
  98. bazimya/stubs/app/bazimya.stub +41 -0
  99. bazimya/stubs/app/bootstrap/app.py.stub +30 -0
  100. bazimya/stubs/app/bootstrap/cache/.gitkeep +0 -0
  101. bazimya/stubs/app/config/app.py.stub +44 -0
  102. bazimya/stubs/app/config/auth.py.stub +28 -0
  103. bazimya/stubs/app/config/cache.py.stub +20 -0
  104. bazimya/stubs/app/config/cors.py.stub +16 -0
  105. bazimya/stubs/app/config/database.py.stub +39 -0
  106. bazimya/stubs/app/config/extensions.py.stub +17 -0
  107. bazimya/stubs/app/config/filesystems.py.stub +27 -0
  108. bazimya/stubs/app/config/hashing.py.stub +22 -0
  109. bazimya/stubs/app/config/logging.py.stub +9 -0
  110. bazimya/stubs/app/config/mail.py.stub +31 -0
  111. bazimya/stubs/app/config/queue.py.stub +20 -0
  112. bazimya/stubs/app/config/services.py.stub +13 -0
  113. bazimya/stubs/app/config/session.py.stub +31 -0
  114. bazimya/stubs/app/config/view.py.stub +12 -0
  115. bazimya/stubs/app/database/.gitkeep +0 -0
  116. bazimya/stubs/app/database/factories/.gitkeep +0 -0
  117. bazimya/stubs/app/database/factories/UserFactory.py.stub +56 -0
  118. bazimya/stubs/app/database/migrations/2026_01_01_000000_create_users_table.py.stub +17 -0
  119. bazimya/stubs/app/database/seeders/DatabaseSeeder.py.stub +11 -0
  120. bazimya/stubs/app/extensions/.gitkeep +0 -0
  121. bazimya/stubs/app/package.json.stub +19 -0
  122. bazimya/stubs/app/passenger_wsgi.py.stub +18 -0
  123. bazimya/stubs/app/public/.htaccess.stub +33 -0
  124. bazimya/stubs/app/public/css/app.css.stub +86 -0
  125. bazimya/stubs/app/public/index.py.stub +19 -0
  126. bazimya/stubs/app/pytest.ini.stub +9 -0
  127. bazimya/stubs/app/requirements.txt.stub +11 -0
  128. bazimya/stubs/app/resources/css/app.css.stub +5 -0
  129. bazimya/stubs/app/resources/js/app.js.stub +4 -0
  130. bazimya/stubs/app/resources/views/auth/login.baz.html.stub +23 -0
  131. bazimya/stubs/app/resources/views/auth/register.baz.html.stub +30 -0
  132. bazimya/stubs/app/resources/views/components/alert.baz.html.stub +6 -0
  133. bazimya/stubs/app/resources/views/components/input-error.baz.html.stub +8 -0
  134. bazimya/stubs/app/resources/views/errors/404.baz.html.stub +9 -0
  135. bazimya/stubs/app/resources/views/errors/500.baz.html.stub +9 -0
  136. bazimya/stubs/app/resources/views/errors/503.baz.html.stub +8 -0
  137. bazimya/stubs/app/resources/views/home.baz.html.stub +45 -0
  138. bazimya/stubs/app/resources/views/layouts/app.baz.html.stub +26 -0
  139. bazimya/stubs/app/routes/api.py.stub +12 -0
  140. bazimya/stubs/app/routes/auth.py.stub +20 -0
  141. bazimya/stubs/app/routes/console.py.stub +12 -0
  142. bazimya/stubs/app/routes/web.py.stub +32 -0
  143. bazimya/stubs/app/storage/app/public/.gitkeep +0 -0
  144. bazimya/stubs/app/storage/framework/cache/.gitkeep +0 -0
  145. bazimya/stubs/app/storage/framework/sessions/.gitkeep +0 -0
  146. bazimya/stubs/app/storage/framework/testing/.gitkeep +0 -0
  147. bazimya/stubs/app/storage/framework/views/.gitkeep +0 -0
  148. bazimya/stubs/app/storage/logs/.gitkeep +0 -0
  149. bazimya/stubs/app/tests/Feature/.gitkeep +0 -0
  150. bazimya/stubs/app/tests/Feature/ExampleTest.py.stub +17 -0
  151. bazimya/stubs/app/tests/Feature/__init__.py.stub +0 -0
  152. bazimya/stubs/app/tests/TestCase.py.stub +21 -0
  153. bazimya/stubs/app/tests/Unit/.gitkeep +0 -0
  154. bazimya/stubs/app/tests/Unit/ExampleTest.py.stub +13 -0
  155. bazimya/stubs/app/tests/Unit/__init__.py.stub +0 -0
  156. bazimya/stubs/app/tests/__init__.py.stub +0 -0
  157. bazimya/stubs/app/wsgi.py.stub +16 -0
  158. bazimya/stubs/command.stub +18 -0
  159. bazimya/stubs/component.stub +8 -0
  160. bazimya/stubs/component.view.stub +3 -0
  161. bazimya/stubs/controller.api.stub +38 -0
  162. bazimya/stubs/controller.resource.stub +42 -0
  163. bazimya/stubs/controller.stub +8 -0
  164. bazimya/stubs/extension.stub +60 -0
  165. bazimya/stubs/extension.view.stub +20 -0
  166. bazimya/stubs/middleware.stub +13 -0
  167. bazimya/stubs/migration.create.stub +13 -0
  168. bazimya/stubs/migration.stub +11 -0
  169. bazimya/stubs/migration.table.stub +13 -0
  170. bazimya/stubs/model.stub +16 -0
  171. bazimya/stubs/notification.stub +17 -0
  172. bazimya/stubs/provider.stub +11 -0
  173. bazimya/stubs/request.stub +19 -0
  174. bazimya/stubs/rule.stub +11 -0
  175. bazimya/stubs/seeder.stub +6 -0
  176. bazimya/support/__init__.py +19 -0
  177. bazimya/support/aliases.py +73 -0
  178. bazimya/support/config.py +103 -0
  179. bazimya/support/env.py +137 -0
  180. bazimya/support/strings.py +116 -0
  181. bazimya/testing.py +381 -0
  182. bazimya/validation.py +340 -0
  183. bazimya/validation_rule.py +23 -0
  184. bazimya/view/__init__.py +14 -0
  185. bazimya/view/compiler.py +851 -0
  186. bazimya/view/components.py +156 -0
  187. bazimya/view/view.py +459 -0
  188. bazimya-0.2.0.dist-info/METADATA +604 -0
  189. bazimya-0.2.0.dist-info/RECORD +193 -0
  190. bazimya-0.2.0.dist-info/WHEEL +5 -0
  191. bazimya-0.2.0.dist-info/entry_points.txt +2 -0
  192. bazimya-0.2.0.dist-info/licenses/LICENSE +21 -0
  193. bazimya-0.2.0.dist-info/top_level.txt +1 -0
bazimya/__init__.py ADDED
@@ -0,0 +1,146 @@
1
+ """Bazimya — a Python web framework with Laravel's structure.
2
+
3
+ Everything an application needs is importable from here:
4
+
5
+ from bazimya import Route, Controller, Model, Schema, Migration, Response
6
+
7
+ The layout, the vocabulary and the lifecycle follow Laravel 10; the language
8
+ is Python, and templates are .baz.html.
9
+ """
10
+
11
+ from .database.migration import Migration, Migrator
12
+ from .database.model import Model, ModelNotFound
13
+ from .database.query import QueryBuilder
14
+ from .database.schema import Blueprint, Schema
15
+ from .console.command import Command
16
+ from .console.registry import Console
17
+ from .extensions.extension import Extension
18
+ from .facades import (
19
+ App,
20
+ Auth,
21
+ Cache,
22
+ Config,
23
+ DB,
24
+ Event,
25
+ Extensions,
26
+ Hash,
27
+ Mail,
28
+ Notify,
29
+ Route,
30
+ Session,
31
+ Storage,
32
+ View,
33
+ app,
34
+ auth,
35
+ back,
36
+ csrf_token,
37
+ old,
38
+ base_path,
39
+ config,
40
+ database_path,
41
+ public_path,
42
+ redirect,
43
+ resource_path,
44
+ route,
45
+ storage_path,
46
+ view,
47
+ )
48
+ from .foundation.application import VERSION, Application
49
+ from .foundation.container import Container
50
+ from .foundation.provider import ServiceProvider
51
+ from .http.controller import Controller
52
+ from .http.form_request import FormRequest
53
+ from .http.exceptions import (
54
+ BadRequest,
55
+ Forbidden,
56
+ HttpException,
57
+ NotFound,
58
+ Unauthorized,
59
+ ValidationException,
60
+ abort,
61
+ abort_if,
62
+ abort_unless,
63
+ )
64
+ from .auth.middleware import Authenticate, RedirectIfAuthenticated
65
+ from .http.middleware import Middleware
66
+ from .notifications.notification import Notification
67
+ from .session.middleware import ShareErrorsFromSession, StartSession, VerifyCsrfToken
68
+ from .view.components import Component
69
+ from .http.request import Request
70
+ from .http.response import Response
71
+ from .support.env import Env, env
72
+ from .validation import Validator, validate
73
+ from .validation_rule import Rule
74
+ from .view.view import markup
75
+
76
+ __version__ = VERSION
77
+
78
+ __all__ = [
79
+ "App",
80
+ "Application",
81
+ "Auth",
82
+ "Authenticate",
83
+ "BadRequest",
84
+ "Cache",
85
+ "Blueprint",
86
+ "Command",
87
+ "Component",
88
+ "Config",
89
+ "Console",
90
+ "Container",
91
+ "Controller",
92
+ "DB",
93
+ "Env",
94
+ "Event",
95
+ "Extension",
96
+ "Extensions",
97
+ "Forbidden",
98
+ "Hash",
99
+ "FormRequest",
100
+ "HttpException",
101
+ "Middleware",
102
+ "Migration",
103
+ "Migrator",
104
+ "Model",
105
+ "ModelNotFound",
106
+ "NotFound",
107
+ "Notification",
108
+ "Notify",
109
+ "QueryBuilder",
110
+ "Request",
111
+ "RedirectIfAuthenticated",
112
+ "Response",
113
+ "Route",
114
+ "Rule",
115
+ "Schema",
116
+ "Session",
117
+ "ServiceProvider",
118
+ "ShareErrorsFromSession",
119
+ "StartSession",
120
+ "Storage",
121
+ "Unauthorized",
122
+ "ValidationException",
123
+ "VerifyCsrfToken",
124
+ "Validator",
125
+ "View",
126
+ "abort",
127
+ "abort_if",
128
+ "abort_unless",
129
+ "app",
130
+ "auth",
131
+ "back",
132
+ "base_path",
133
+ "config",
134
+ "csrf_token",
135
+ "database_path",
136
+ "env",
137
+ "markup",
138
+ "old",
139
+ "public_path",
140
+ "redirect",
141
+ "resource_path",
142
+ "route",
143
+ "storage_path",
144
+ "validate",
145
+ "view",
146
+ ]
bazimya/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """`python -m bazimya` — the same entry point as the `bazimya` script."""
2
+
3
+ import sys
4
+
5
+ from .console.kernel import Kernel
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(Kernel().run(sys.argv))
@@ -0,0 +1,12 @@
1
+ """Authentication: the session guard and its middleware."""
2
+
3
+ from .guard import AuthenticationError, SessionGuard
4
+ from .middleware import Authenticate, RedirectIfAuthenticated, ResolveUser
5
+
6
+ __all__ = [
7
+ "Authenticate",
8
+ "AuthenticationError",
9
+ "RedirectIfAuthenticated",
10
+ "ResolveUser",
11
+ "SessionGuard",
12
+ ]
bazimya/auth/guard.py ADDED
@@ -0,0 +1,251 @@
1
+ """Authentication.
2
+
3
+ Laravel's Auth facade over a session guard:
4
+
5
+ if Auth.attempt({'email': email, 'password': password}):
6
+ request.session().regenerate()
7
+
8
+ return redirect('/dashboard')
9
+
10
+ Auth.user() the logged-in model, or None
11
+ Auth.check() True when someone is logged in
12
+ Auth.logout()
13
+
14
+ The guard reads its user model from config/auth.py, so it works with whatever
15
+ model you point it at rather than assuming `User`.
16
+ """
17
+
18
+
19
+ class AuthenticationError(RuntimeError):
20
+ pass
21
+
22
+
23
+ class SessionGuard:
24
+ """Keeps the authenticated user's id in the session."""
25
+
26
+ SESSION_KEY = "_auth_id"
27
+ PASSWORD_HASH_KEY = "_auth_password"
28
+
29
+ def __init__(self, application, config=None):
30
+ self.app = application
31
+ self.config = dict(config or {})
32
+ self._user = None
33
+ self._resolved = False
34
+ self._session = None
35
+
36
+ # -- wiring -----------------------------------------------------------
37
+
38
+ def set_session(self, session):
39
+ """Called by StartSession, once per request."""
40
+ self._session = session
41
+ self._user = None
42
+ self._resolved = False
43
+
44
+ return self
45
+
46
+ def session(self):
47
+ return self._session
48
+
49
+ def provider_model(self):
50
+ """The model this guard authenticates, named in config/auth.py."""
51
+ reference = self.config.get("model", "app.Models.User.User")
52
+
53
+ if not isinstance(reference, str):
54
+ return reference
55
+
56
+ import importlib
57
+
58
+ module_name, _, class_name = reference.rpartition(".")
59
+
60
+ try:
61
+ module = importlib.import_module(module_name)
62
+ except ImportError as error:
63
+ raise AuthenticationError(
64
+ "The auth model [{}] could not be imported. Check "
65
+ "auth.providers.users.model in config/auth.py.".format(reference)
66
+ ) from error
67
+
68
+ model = getattr(module, class_name, None)
69
+
70
+ if model is None:
71
+ raise AuthenticationError(
72
+ "The auth model [{}] was not found.".format(reference)
73
+ )
74
+
75
+ return model
76
+
77
+ def hasher(self):
78
+ return self.app.make("hash")
79
+
80
+ # -- reading the current user -----------------------------------------
81
+
82
+ def user(self):
83
+ if self._resolved:
84
+ return self._user
85
+
86
+ self._resolved = True
87
+
88
+ if self._session is None:
89
+ return None
90
+
91
+ identifier = self._session.get(self.SESSION_KEY)
92
+
93
+ if identifier is None:
94
+ return None
95
+
96
+ model = self.provider_model()
97
+ user = model.find(identifier)
98
+
99
+ # A password change should end other sessions. The stored fragment is
100
+ # compared, so an old cookie stops working once the hash changes.
101
+ if user is not None and not self._password_still_valid(user):
102
+ self._session.forget(self.SESSION_KEY, self.PASSWORD_HASH_KEY)
103
+
104
+ return None
105
+
106
+ self._user = user
107
+
108
+ return user
109
+
110
+ def _password_still_valid(self, user):
111
+ remembered = self._session.get(self.PASSWORD_HASH_KEY)
112
+
113
+ if remembered is None:
114
+ return True
115
+
116
+ return remembered == self._password_fingerprint(user)
117
+
118
+ @staticmethod
119
+ def _password_fingerprint(user):
120
+ import hashlib
121
+
122
+ password = user.attributes().get("password") or ""
123
+
124
+ return hashlib.sha256(str(password).encode("utf-8")).hexdigest()[:32]
125
+
126
+ def id(self):
127
+ user = self.user()
128
+
129
+ return user.key() if user is not None else None
130
+
131
+ def check(self):
132
+ return self.user() is not None
133
+
134
+ def guest(self):
135
+ return not self.check()
136
+
137
+ # -- logging in and out -----------------------------------------------
138
+
139
+ def attempt(self, credentials, remember=False):
140
+ """Find a matching user and verify the password.
141
+
142
+ Returns True on success. The password is verified even when no user
143
+ matched, so that a wrong email and a wrong password take the same time
144
+ and the response cannot be used to enumerate accounts.
145
+ """
146
+ credentials = dict(credentials or {})
147
+ password = credentials.pop("password", None)
148
+
149
+ user = self.retrieve_by_credentials(credentials)
150
+
151
+ if user is None:
152
+ self.hasher().check(str(password or ""), _DUMMY_HASH)
153
+
154
+ return False
155
+
156
+ if not self.validate_password(user, password):
157
+ return False
158
+
159
+ self.login(user, remember)
160
+
161
+ return True
162
+
163
+ def retrieve_by_credentials(self, credentials):
164
+ if not credentials:
165
+ return None
166
+
167
+ model = self.provider_model()
168
+ query = model.query()
169
+
170
+ for column, value in credentials.items():
171
+ query = query.where(column, value)
172
+
173
+ return query.first()
174
+
175
+ def validate_password(self, user, password):
176
+ stored = user.attributes().get("password")
177
+
178
+ if not stored:
179
+ return False
180
+
181
+ if not self.hasher().check(str(password or ""), stored):
182
+ return False
183
+
184
+ # Upgrade the stored hash when the configured cost has moved on. This
185
+ # is the only moment the plaintext is available to do it.
186
+ if self.hasher().needs_rehash(stored):
187
+ user.force_fill({"password": self.hasher().make(password)})
188
+ user.save()
189
+
190
+ return True
191
+
192
+ def login(self, user, remember=False):
193
+ if self._session is None:
194
+ raise AuthenticationError(
195
+ "There is no session to log in to. Add StartSession to the "
196
+ "middleware group for this route."
197
+ )
198
+
199
+ # A new session id on login is what closes off session fixation: any
200
+ # id an attacker planted before login stops being the one in use.
201
+ self._session.regenerate()
202
+ self._session.put(self.SESSION_KEY, user.key())
203
+ self._session.put(self.PASSWORD_HASH_KEY, self._password_fingerprint(user))
204
+
205
+ self._user = user
206
+ self._resolved = True
207
+
208
+ return user
209
+
210
+ def login_using_id(self, identifier):
211
+ user = self.provider_model().find(identifier)
212
+
213
+ return self.login(user) if user is not None else None
214
+
215
+ def once(self, credentials):
216
+ """Verify credentials without touching the session."""
217
+ user = self.retrieve_by_credentials(
218
+ {k: v for k, v in credentials.items() if k != "password"}
219
+ )
220
+
221
+ if user is None or not self.validate_password(user, credentials.get("password")):
222
+ return False
223
+
224
+ self._user = user
225
+ self._resolved = True
226
+
227
+ return True
228
+
229
+ def logout(self):
230
+ if self._session is not None:
231
+ self._session.forget(self.SESSION_KEY, self.PASSWORD_HASH_KEY)
232
+ self._session.invalidate()
233
+
234
+ self._user = None
235
+ self._resolved = True
236
+
237
+ return self
238
+
239
+ def set_user(self, user):
240
+ self._user = user
241
+ self._resolved = True
242
+
243
+ return self
244
+
245
+
246
+ #: A real hash of a random value, so a failed lookup still costs one
247
+ #: verification and the timing of "no such user" matches "wrong password".
248
+ _DUMMY_HASH = (
249
+ "pbkdf2$480000$AAAAAAAAAAAAAAAAAAAAAA$"
250
+ "cGxhY2Vob2xkZXJwbGFjZWhvbGRlcnBsYWNlaG9sZGVy"
251
+ )
@@ -0,0 +1,76 @@
1
+ """Authentication middleware — Laravel's Authenticate and RedirectIfAuthenticated."""
2
+
3
+ from ..http.exceptions import Unauthorized
4
+ from ..http.middleware import Middleware
5
+ from ..http.response import Response
6
+
7
+
8
+ class Authenticate(Middleware):
9
+ """Stop guests. Registered as the `auth` alias.
10
+
11
+ Browsers get a redirect to the login page; anything asking for JSON gets a
12
+ 401, because redirecting an API client to an HTML form is never useful.
13
+ """
14
+
15
+ redirect_to = "/login"
16
+
17
+ def handle(self, request, next):
18
+ from ..facades import Auth
19
+
20
+ session = request.get("session")
21
+
22
+ if session is not None:
23
+ Auth.set_session(session)
24
+
25
+ user = Auth.user()
26
+
27
+ if user is None:
28
+ if request.wants_json():
29
+ raise Unauthorized("Unauthenticated.")
30
+
31
+ if session is not None:
32
+ # Remember where they were headed, so login can send them back.
33
+ session.put("_intended", request.full_url())
34
+
35
+ return Response.redirect(self.redirect_to)
36
+
37
+ request.set("user", user)
38
+
39
+ return next(request)
40
+
41
+
42
+ class RedirectIfAuthenticated(Middleware):
43
+ """Keep logged-in users off the login and register pages. Alias: `guest`."""
44
+
45
+ redirect_to = "/"
46
+
47
+ def handle(self, request, next):
48
+ from ..facades import Auth
49
+
50
+ session = request.get("session")
51
+
52
+ if session is not None:
53
+ Auth.set_session(session)
54
+
55
+ if Auth.check():
56
+ return Response.redirect(self.redirect_to)
57
+
58
+ return next(request)
59
+
60
+
61
+ class ResolveUser(Middleware):
62
+ """Attach the authenticated user to the request without requiring one.
63
+
64
+ For pages that show a name when signed in and a login link otherwise.
65
+ """
66
+
67
+ def handle(self, request, next):
68
+ from ..facades import Auth
69
+
70
+ session = request.get("session")
71
+
72
+ if session is not None:
73
+ Auth.set_session(session)
74
+ request.set("user", Auth.user())
75
+
76
+ return next(request)
@@ -0,0 +1,5 @@
1
+ """Caching."""
2
+
3
+ from .repository import ArrayStore, CacheRepository, FileStore, NullStore
4
+
5
+ __all__ = ["ArrayStore", "CacheRepository", "FileStore", "NullStore"]
@@ -0,0 +1,206 @@
1
+ """The cache.
2
+
3
+ Laravel's Cache facade, with a file store under storage/framework/cache. No
4
+ Redis, no Memcached — the file store needs nothing installed, which is the
5
+ whole point on shared hosting.
6
+
7
+ Cache.put('key', value, seconds=600)
8
+ Cache.get('key', default)
9
+ Cache.remember('key', 600, lambda: expensive())
10
+ """
11
+
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import time
16
+
17
+
18
+ class FileStore:
19
+ def __init__(self, directory):
20
+ self.directory = directory
21
+
22
+ def _file(self, key):
23
+ safe = hashlib.sha256(str(key).encode("utf-8")).hexdigest()
24
+
25
+ # Two levels of subdirectory, so a large cache does not put tens of
26
+ # thousands of files in one directory.
27
+ return os.path.join(self.directory, safe[:2], safe[2:4], safe)
28
+
29
+ def get(self, key, default=None):
30
+ path = self._file(key)
31
+
32
+ try:
33
+ with open(path, "r", encoding="utf-8") as handle:
34
+ payload = json.load(handle)
35
+ except (OSError, ValueError):
36
+ return default
37
+
38
+ expires = payload.get("expires")
39
+
40
+ if expires is not None and expires < time.time():
41
+ self.forget(key)
42
+
43
+ return default
44
+
45
+ return payload.get("value", default)
46
+
47
+ def put(self, key, value, seconds=None):
48
+ path = self._file(key)
49
+
50
+ try:
51
+ os.makedirs(os.path.dirname(path), exist_ok=True)
52
+
53
+ payload = {
54
+ "value": value,
55
+ "expires": time.time() + seconds if seconds else None,
56
+ }
57
+
58
+ temporary = "{}.{}.tmp".format(path, os.getpid())
59
+
60
+ with open(temporary, "w", encoding="utf-8") as handle:
61
+ json.dump(payload, handle, default=str)
62
+
63
+ os.replace(temporary, path)
64
+
65
+ return True
66
+ except (OSError, TypeError, ValueError):
67
+ # A cache that cannot write is slow, not broken.
68
+ return False
69
+
70
+ def forget(self, key):
71
+ try:
72
+ os.remove(self._file(key))
73
+
74
+ return True
75
+ except OSError:
76
+ return False
77
+
78
+ def flush(self):
79
+ import shutil
80
+
81
+ try:
82
+ shutil.rmtree(self.directory)
83
+ os.makedirs(self.directory, exist_ok=True)
84
+
85
+ return True
86
+ except OSError:
87
+ return False
88
+
89
+
90
+ class ArrayStore:
91
+ """In-memory, per-process. The default while testing."""
92
+
93
+ def __init__(self):
94
+ self._items = {}
95
+
96
+ def get(self, key, default=None):
97
+ entry = self._items.get(key)
98
+
99
+ if entry is None:
100
+ return default
101
+
102
+ value, expires = entry
103
+
104
+ if expires is not None and expires < time.time():
105
+ self._items.pop(key, None)
106
+
107
+ return default
108
+
109
+ return value
110
+
111
+ def put(self, key, value, seconds=None):
112
+ self._items[key] = (value, time.time() + seconds if seconds else None)
113
+
114
+ return True
115
+
116
+ def forget(self, key):
117
+ return self._items.pop(key, None) is not None
118
+
119
+ def flush(self):
120
+ self._items = {}
121
+
122
+ return True
123
+
124
+
125
+ class NullStore:
126
+ """Stores nothing. Useful for turning caching off without code changes."""
127
+
128
+ def get(self, key, default=None):
129
+ return default
130
+
131
+ def put(self, key, value, seconds=None):
132
+ return True
133
+
134
+ def forget(self, key):
135
+ return True
136
+
137
+ def flush(self):
138
+ return True
139
+
140
+
141
+ class CacheRepository:
142
+ def __init__(self, store):
143
+ self.store = store
144
+
145
+ def get(self, key, default=None):
146
+ return self.store.get(key, default)
147
+
148
+ def put(self, key, value, seconds=None):
149
+ return self.store.put(key, value, seconds)
150
+
151
+ def add(self, key, value, seconds=None):
152
+ """Store it only if it is not there already."""
153
+ sentinel = object()
154
+
155
+ if self.store.get(key, sentinel) is not sentinel:
156
+ return False
157
+
158
+ return self.store.put(key, value, seconds)
159
+
160
+ def forever(self, key, value):
161
+ return self.store.put(key, value, None)
162
+
163
+ def has(self, key):
164
+ sentinel = object()
165
+
166
+ return self.store.get(key, sentinel) is not sentinel
167
+
168
+ def missing(self, key):
169
+ return not self.has(key)
170
+
171
+ def pull(self, key, default=None):
172
+ value = self.get(key, default)
173
+ self.forget(key)
174
+
175
+ return value
176
+
177
+ def remember(self, key, seconds, callback):
178
+ """Return the cached value, computing and storing it when absent."""
179
+ sentinel = object()
180
+ value = self.store.get(key, sentinel)
181
+
182
+ if value is not sentinel:
183
+ return value
184
+
185
+ value = callback()
186
+ self.store.put(key, value, seconds)
187
+
188
+ return value
189
+
190
+ def remember_forever(self, key, callback):
191
+ return self.remember(key, None, callback)
192
+
193
+ def increment(self, key, amount=1):
194
+ value = int(self.get(key, 0) or 0) + amount
195
+ self.forever(key, value)
196
+
197
+ return value
198
+
199
+ def decrement(self, key, amount=1):
200
+ return self.increment(key, -amount)
201
+
202
+ def forget(self, key):
203
+ return self.store.forget(key)
204
+
205
+ def flush(self):
206
+ return self.store.flush()
@@ -0,0 +1,7 @@
1
+ """The command line."""
2
+
3
+ from .command import Command
4
+ from .kernel import Kernel, main
5
+ from .output import Output
6
+
7
+ __all__ = ["Command", "Kernel", "Output", "main"]