pylara 1.0.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.
- pylara-1.0.0/LICENSE +21 -0
- pylara-1.0.0/MANIFEST.in +6 -0
- pylara-1.0.0/PKG-INFO +86 -0
- pylara-1.0.0/README.md +58 -0
- pylara-1.0.0/docs/cheatsheet.md +62 -0
- pylara-1.0.0/docs/configuration.md +84 -0
- pylara-1.0.0/docs/console.md +85 -0
- pylara-1.0.0/docs/container.md +80 -0
- pylara-1.0.0/docs/contributing.md +57 -0
- pylara-1.0.0/docs/controllers.md +87 -0
- pylara-1.0.0/docs/csrf.md +112 -0
- pylara-1.0.0/docs/database.md +75 -0
- pylara-1.0.0/docs/deployment.md +110 -0
- pylara-1.0.0/docs/errors.md +99 -0
- pylara-1.0.0/docs/helpers.md +87 -0
- pylara-1.0.0/docs/index.md +51 -0
- pylara-1.0.0/docs/installation.md +72 -0
- pylara-1.0.0/docs/lifecycle.md +42 -0
- pylara-1.0.0/docs/limitations.md +58 -0
- pylara-1.0.0/docs/middleware.md +123 -0
- pylara-1.0.0/docs/migrations.md +129 -0
- pylara-1.0.0/docs/orm.md +191 -0
- pylara-1.0.0/docs/providers.md +55 -0
- pylara-1.0.0/docs/query-builder.md +90 -0
- pylara-1.0.0/docs/requests.md +81 -0
- pylara-1.0.0/docs/responses.md +91 -0
- pylara-1.0.0/docs/routing.md +165 -0
- pylara-1.0.0/docs/sessions.md +161 -0
- pylara-1.0.0/docs/structure.md +69 -0
- pylara-1.0.0/docs/testing.md +135 -0
- pylara-1.0.0/docs/uploads.md +119 -0
- pylara-1.0.0/docs/validation.md +78 -0
- pylara-1.0.0/docs/views.md +160 -0
- pylara-1.0.0/examples/demo/README.md +19 -0
- pylara-1.0.0/examples/demo/app/__init__.py +0 -0
- pylara-1.0.0/examples/demo/app/controllers/__init__.py +0 -0
- pylara-1.0.0/examples/demo/app/controllers/auth_controller.py +60 -0
- pylara-1.0.0/examples/demo/app/controllers/post_controller.py +40 -0
- pylara-1.0.0/examples/demo/app/controllers/user_controller.py +39 -0
- pylara-1.0.0/examples/demo/app/middleware/__init__.py +0 -0
- pylara-1.0.0/examples/demo/app/models/__init__.py +0 -0
- pylara-1.0.0/examples/demo/app/models/post.py +11 -0
- pylara-1.0.0/examples/demo/app/models/user.py +12 -0
- pylara-1.0.0/examples/demo/bootstrap/__init__.py +0 -0
- pylara-1.0.0/examples/demo/bootstrap/app.py +53 -0
- pylara-1.0.0/examples/demo/config/app.json +4 -0
- pylara-1.0.0/examples/demo/database/__init__.py +0 -0
- pylara-1.0.0/examples/demo/database/migrations/2024_01_01_000000_create_users_table.py +16 -0
- pylara-1.0.0/examples/demo/database/migrations/2024_01_01_000001_create_posts_table.py +17 -0
- pylara-1.0.0/examples/demo/database/migrations/__init__.py +0 -0
- pylara-1.0.0/examples/demo/manage.py +24 -0
- pylara-1.0.0/examples/demo/routes/__init__.py +0 -0
- pylara-1.0.0/examples/demo/routes/web.py +62 -0
- pylara-1.0.0/examples/tracker/.env.example +10 -0
- pylara-1.0.0/examples/tracker/.gitignore +4 -0
- pylara-1.0.0/examples/tracker/README.md +17 -0
- pylara-1.0.0/examples/tracker/app/__init__.py +0 -0
- pylara-1.0.0/examples/tracker/app/controllers/__init__.py +0 -0
- pylara-1.0.0/examples/tracker/app/controllers/auth_controller.py +41 -0
- pylara-1.0.0/examples/tracker/app/controllers/task_controller.py +58 -0
- pylara-1.0.0/examples/tracker/app/controllers/welcome_controller.py +10 -0
- pylara-1.0.0/examples/tracker/app/middleware/__init__.py +0 -0
- pylara-1.0.0/examples/tracker/app/models/__init__.py +0 -0
- pylara-1.0.0/examples/tracker/app/models/task.py +7 -0
- pylara-1.0.0/examples/tracker/app/models/user.py +7 -0
- pylara-1.0.0/examples/tracker/app/providers/__init__.py +0 -0
- pylara-1.0.0/examples/tracker/app/providers/app_service_provider.py +11 -0
- pylara-1.0.0/examples/tracker/bootstrap/__init__.py +0 -0
- pylara-1.0.0/examples/tracker/bootstrap/app.py +34 -0
- pylara-1.0.0/examples/tracker/config/app.json +4 -0
- pylara-1.0.0/examples/tracker/database/migrations/2026_11_15_031555_create_tasks_table.py +18 -0
- pylara-1.0.0/examples/tracker/database/migrations/2026_11_15_031555_create_users_table.py +16 -0
- pylara-1.0.0/examples/tracker/manage.py +17 -0
- pylara-1.0.0/examples/tracker/public/css/app.css +11 -0
- pylara-1.0.0/examples/tracker/requirements.txt +1 -0
- pylara-1.0.0/examples/tracker/resources/views/auth/login.html +11 -0
- pylara-1.0.0/examples/tracker/resources/views/auth/register.html +15 -0
- pylara-1.0.0/examples/tracker/resources/views/layouts/app.html +15 -0
- pylara-1.0.0/examples/tracker/resources/views/tasks/index.html +28 -0
- pylara-1.0.0/examples/tracker/resources/views/welcome.html +11 -0
- pylara-1.0.0/examples/tracker/routes/__init__.py +0 -0
- pylara-1.0.0/examples/tracker/routes/api.py +10 -0
- pylara-1.0.0/examples/tracker/routes/web.py +21 -0
- pylara-1.0.0/examples/tracker/storage/.gitignore +2 -0
- pylara-1.0.0/examples/tracker/tests/__init__.py +6 -0
- pylara-1.0.0/examples/tracker/tests/test_example.py +36 -0
- pylara-1.0.0/examples/tracker/tests/test_tracker.py +135 -0
- pylara-1.0.0/examples/tracker/wsgi.py +11 -0
- pylara-1.0.0/pylara/__init__.py +23 -0
- pylara-1.0.0/pylara/__main__.py +6 -0
- pylara-1.0.0/pylara/application.py +297 -0
- pylara-1.0.0/pylara/cli.py +108 -0
- pylara-1.0.0/pylara/console/__init__.py +0 -0
- pylara-1.0.0/pylara/console/console.py +422 -0
- pylara-1.0.0/pylara/database/__init__.py +0 -0
- pylara-1.0.0/pylara/database/migration.py +305 -0
- pylara-1.0.0/pylara/http/__init__.py +3 -0
- pylara-1.0.0/pylara/http/context.py +17 -0
- pylara-1.0.0/pylara/http/controller.py +35 -0
- pylara-1.0.0/pylara/http/exceptions.py +38 -0
- pylara-1.0.0/pylara/http/kernel.py +86 -0
- pylara-1.0.0/pylara/http/request.py +271 -0
- pylara-1.0.0/pylara/http/response.py +110 -0
- pylara-1.0.0/pylara/http/uploads.py +81 -0
- pylara-1.0.0/pylara/middleware/__init__.py +0 -0
- pylara-1.0.0/pylara/middleware/middleware.py +106 -0
- pylara-1.0.0/pylara/middleware/session.py +93 -0
- pylara-1.0.0/pylara/orm/__init__.py +0 -0
- pylara-1.0.0/pylara/orm/model.py +515 -0
- pylara-1.0.0/pylara/routing/__init__.py +0 -0
- pylara-1.0.0/pylara/routing/router.py +289 -0
- pylara-1.0.0/pylara/stubs/project/.env.example.stub +10 -0
- pylara-1.0.0/pylara/stubs/project/.env.stub +11 -0
- pylara-1.0.0/pylara/stubs/project/.gitignore.stub +4 -0
- pylara-1.0.0/pylara/stubs/project/README.md.stub +42 -0
- pylara-1.0.0/pylara/stubs/project/app/__init__.py.stub +0 -0
- pylara-1.0.0/pylara/stubs/project/app/controllers/__init__.py.stub +0 -0
- pylara-1.0.0/pylara/stubs/project/app/controllers/welcome_controller.py.stub +10 -0
- pylara-1.0.0/pylara/stubs/project/app/middleware/__init__.py.stub +0 -0
- pylara-1.0.0/pylara/stubs/project/app/models/__init__.py.stub +0 -0
- pylara-1.0.0/pylara/stubs/project/app/models/user.py.stub +7 -0
- pylara-1.0.0/pylara/stubs/project/app/providers/__init__.py.stub +0 -0
- pylara-1.0.0/pylara/stubs/project/app/providers/app_service_provider.py.stub +11 -0
- pylara-1.0.0/pylara/stubs/project/bootstrap/__init__.py.stub +0 -0
- pylara-1.0.0/pylara/stubs/project/bootstrap/app.py.stub +34 -0
- pylara-1.0.0/pylara/stubs/project/config/app.json.stub +4 -0
- pylara-1.0.0/pylara/stubs/project/database/migrations/__MIGRATION_TS___create_users_table.py.stub +16 -0
- pylara-1.0.0/pylara/stubs/project/manage.py.stub +17 -0
- pylara-1.0.0/pylara/stubs/project/public/css/app.css.stub +11 -0
- pylara-1.0.0/pylara/stubs/project/requirements.txt.stub +1 -0
- pylara-1.0.0/pylara/stubs/project/resources/views/layouts/app.html.stub +14 -0
- pylara-1.0.0/pylara/stubs/project/resources/views/welcome.html.stub +11 -0
- pylara-1.0.0/pylara/stubs/project/routes/__init__.py.stub +0 -0
- pylara-1.0.0/pylara/stubs/project/routes/api.py.stub +10 -0
- pylara-1.0.0/pylara/stubs/project/routes/web.py.stub +10 -0
- pylara-1.0.0/pylara/stubs/project/storage/.gitignore.stub +2 -0
- pylara-1.0.0/pylara/stubs/project/tests/__init__.py.stub +6 -0
- pylara-1.0.0/pylara/stubs/project/tests/test_example.py.stub +36 -0
- pylara-1.0.0/pylara/stubs/project/wsgi.py.stub +11 -0
- pylara-1.0.0/pylara/support/__init__.py +0 -0
- pylara-1.0.0/pylara/support/config.py +55 -0
- pylara-1.0.0/pylara/support/hashing.py +32 -0
- pylara-1.0.0/pylara/support/helpers.py +218 -0
- pylara-1.0.0/pylara/support/service_container.py +100 -0
- pylara-1.0.0/pylara/support/service_provider.py +23 -0
- pylara-1.0.0/pylara/support/session.py +206 -0
- pylara-1.0.0/pylara/support/view.py +261 -0
- pylara-1.0.0/pylara/testing.py +141 -0
- pylara-1.0.0/pylara/version.py +3 -0
- pylara-1.0.0/pylara.egg-info/PKG-INFO +86 -0
- pylara-1.0.0/pylara.egg-info/SOURCES.txt +160 -0
- pylara-1.0.0/pylara.egg-info/dependency_links.txt +1 -0
- pylara-1.0.0/pylara.egg-info/entry_points.txt +2 -0
- pylara-1.0.0/pylara.egg-info/top_level.txt +1 -0
- pylara-1.0.0/pyproject.toml +48 -0
- pylara-1.0.0/setup.cfg +4 -0
- pylara-1.0.0/tests/__init__.py +0 -0
- pylara-1.0.0/tests/__main__.py +9 -0
- pylara-1.0.0/tests/test_docs.py +229 -0
- pylara-1.0.0/tests/test_features.py +739 -0
- pylara-1.0.0/tests/test_framework.py +639 -0
- pylara-1.0.0/tests/test_security.py +472 -0
pylara-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The PyLara Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pylara-1.0.0/MANIFEST.in
ADDED
pylara-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pylara
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Laravel-inspired web framework for Python: routing, Eloquent-style ORM, middleware, migrations and an Artisan-style CLI.
|
|
5
|
+
Author-email: Dzulfikar Maulana <dzulfikar.maulana@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gekif/pylara
|
|
8
|
+
Project-URL: Documentation, https://github.com/gekif/pylara/tree/main/docs
|
|
9
|
+
Project-URL: Source, https://github.com/gekif/pylara
|
|
10
|
+
Project-URL: Issues, https://github.com/gekif/pylara/issues
|
|
11
|
+
Keywords: laravel,framework,web,wsgi,orm,mvc,artisan
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# PyLara
|
|
30
|
+
|
|
31
|
+
A Laravel-inspired web framework for Python. Routing, an Eloquent-style ORM, middleware,
|
|
32
|
+
migrations, a small template engine, validation, sessions, CSRF protection, file uploads, and an
|
|
33
|
+
Artisan-style CLI, with **no dependencies** beyond the standard library.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install pylara
|
|
37
|
+
pylara new blog
|
|
38
|
+
cd blog
|
|
39
|
+
python -m manage migrate
|
|
40
|
+
python -m manage serve
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Open http://127.0.0.1:8000.
|
|
44
|
+
|
|
45
|
+
> **Status:** 1.0 is the first release. PyLara is synchronous (WSGI), targets SQLite, and is
|
|
46
|
+
> best suited to learning, prototypes, internal tools and small APIs. Read
|
|
47
|
+
> [Limitations](https://github.com/YOUR-USER/pylara/blob/main/docs/limitations.md) before choosing it for production.
|
|
48
|
+
|
|
49
|
+
## A taste of PyLara
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from pylara import Application, Controller, Model
|
|
53
|
+
|
|
54
|
+
class Task(Model):
|
|
55
|
+
table = "tasks"
|
|
56
|
+
fillable = ["title", "done"]
|
|
57
|
+
|
|
58
|
+
class TaskController(Controller):
|
|
59
|
+
def index(self, request):
|
|
60
|
+
return self.json([t.to_dict() for t in Task.where("done", 0).order_by("id", "DESC").get()])
|
|
61
|
+
|
|
62
|
+
def store(self, request):
|
|
63
|
+
data = request.validate({"title": "required|string|min:3|max:150"})
|
|
64
|
+
return self.json(Task.create(title=data["title"], done=0).to_dict(), 201)
|
|
65
|
+
|
|
66
|
+
app = Application()
|
|
67
|
+
app.group({"prefix": "/api"}, lambda r: [r.resource("tasks", TaskController)])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
Start at **[docs/index.md](https://github.com/YOUR-USER/pylara/blob/main/docs/index.md)**: installation, configuration, routing, controllers,
|
|
73
|
+
requests, responses, views, validation, the database layer and ORM, testing, deployment and
|
|
74
|
+
publishing your own package.
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git clone <your fork> && cd pylara
|
|
80
|
+
python -m pip install -e .
|
|
81
|
+
python -m tests # run the whole test suite
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT. See [LICENSE](https://github.com/YOUR-USER/pylara/blob/main/LICENSE).
|
pylara-1.0.0/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# PyLara
|
|
2
|
+
|
|
3
|
+
A Laravel-inspired web framework for Python. Routing, an Eloquent-style ORM, middleware,
|
|
4
|
+
migrations, a small template engine, validation, sessions, CSRF protection, file uploads, and an
|
|
5
|
+
Artisan-style CLI, with **no dependencies** beyond the standard library.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install pylara
|
|
9
|
+
pylara new blog
|
|
10
|
+
cd blog
|
|
11
|
+
python -m manage migrate
|
|
12
|
+
python -m manage serve
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Open http://127.0.0.1:8000.
|
|
16
|
+
|
|
17
|
+
> **Status:** 1.0 is the first release. PyLara is synchronous (WSGI), targets SQLite, and is
|
|
18
|
+
> best suited to learning, prototypes, internal tools and small APIs. Read
|
|
19
|
+
> [Limitations](https://github.com/YOUR-USER/pylara/blob/main/docs/limitations.md) before choosing it for production.
|
|
20
|
+
|
|
21
|
+
## A taste of PyLara
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from pylara import Application, Controller, Model
|
|
25
|
+
|
|
26
|
+
class Task(Model):
|
|
27
|
+
table = "tasks"
|
|
28
|
+
fillable = ["title", "done"]
|
|
29
|
+
|
|
30
|
+
class TaskController(Controller):
|
|
31
|
+
def index(self, request):
|
|
32
|
+
return self.json([t.to_dict() for t in Task.where("done", 0).order_by("id", "DESC").get()])
|
|
33
|
+
|
|
34
|
+
def store(self, request):
|
|
35
|
+
data = request.validate({"title": "required|string|min:3|max:150"})
|
|
36
|
+
return self.json(Task.create(title=data["title"], done=0).to_dict(), 201)
|
|
37
|
+
|
|
38
|
+
app = Application()
|
|
39
|
+
app.group({"prefix": "/api"}, lambda r: [r.resource("tasks", TaskController)])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Documentation
|
|
43
|
+
|
|
44
|
+
Start at **[docs/index.md](https://github.com/YOUR-USER/pylara/blob/main/docs/index.md)**: installation, configuration, routing, controllers,
|
|
45
|
+
requests, responses, views, validation, the database layer and ORM, testing, deployment and
|
|
46
|
+
publishing your own package.
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone <your fork> && cd pylara
|
|
52
|
+
python -m pip install -e .
|
|
53
|
+
python -m tests # run the whole test suite
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT. See [LICENSE](https://github.com/YOUR-USER/pylara/blob/main/LICENSE).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Laravel Cheat Sheet
|
|
2
|
+
|
|
3
|
+
| Laravel (PHP) | PyLara (Python) |
|
|
4
|
+
|---|---|
|
|
5
|
+
| `composer create-project laravel/laravel blog` | `pylara new blog` |
|
|
6
|
+
| `php artisan serve` | `python -m manage serve` |
|
|
7
|
+
| `php artisan route:list` | `python -m manage route:list` |
|
|
8
|
+
| `php artisan make:controller PostController --resource` | `python -m manage make:controller PostController --resource` |
|
|
9
|
+
| `php artisan make:model Post -m` | `python -m manage make:model Post -m` |
|
|
10
|
+
| `php artisan make:middleware X` | `python -m manage make:middleware X` |
|
|
11
|
+
| `php artisan migrate` / `migrate:rollback` / `migrate:status` | `python -m manage migrate` / `migrate:rollback` / `migrate:status` |
|
|
12
|
+
| `Route::get('/u/{id}', [C::class, 'show'])` | `app.get("/u/{id}", [C, "show"])` |
|
|
13
|
+
| `Route::resource('posts', PostController::class)` | `app.resource("posts", PostController)` |
|
|
14
|
+
| `Route::get(...)->name('x')` | `app.get(...).named("x")` |
|
|
15
|
+
| `Route::get(...)->middleware(M::class)` | `app.get(...).with_middleware(M)` |
|
|
16
|
+
| `Route::prefix('api')->group(fn)` | `app.group({"prefix": "/api"}, fn)` |
|
|
17
|
+
| `route('x', ['id' => 1])` | `route("x", id=1)` |
|
|
18
|
+
| `$request->input('a')`, `$request->all()` | `request.input("a")`, `request.all()` |
|
|
19
|
+
| `$request->only(['a','b'])` | `request.only("a", "b")` |
|
|
20
|
+
| `$request->validate(['a' => 'required\|email'])` | `request.validate({"a": "required\|email"})` |
|
|
21
|
+
| `return response()->json($x, 201)` | `return JsonResponse(x, 201)` or `self.json(x, 201)` |
|
|
22
|
+
| `return redirect('/home')` | `return RedirectResponse("/home")` or `self.redirect("/home")` |
|
|
23
|
+
| `abort(404)` | `abort(404)` |
|
|
24
|
+
| `view('welcome', ['a' => 1])` | `view("welcome", a=1)` |
|
|
25
|
+
| Blade `{{ $x }}`, `{!! $x !!}` | `{{ x }}`, `{!! x !!}` |
|
|
26
|
+
| Blade `@if`, `@foreach`, `@extends`, `@section`, `@include` | `{% if %}`, `{% for %}`, `{% extends %}`, `{% block %}`, `{% include %}` |
|
|
27
|
+
| `config('app.name')` | `config("app.name")` |
|
|
28
|
+
| `env('APP_DEBUG', false)` | `env("APP_DEBUG", False)` |
|
|
29
|
+
| `User::find(1)`, `User::findOrFail(1)` | `User.find(1)`, `User.find_or_fail(1)` |
|
|
30
|
+
| `User::where('a', 1)->first()` | `User.where("a", 1).first()` |
|
|
31
|
+
| `User::create([...])` | `User.create(**data)` |
|
|
32
|
+
| `$user->save()`, `$user->delete()` | `user.save()`, `user.delete()` |
|
|
33
|
+
| `$fillable`, `$hidden`, `$casts`, `SoftDeletes` | `fillable`, `hidden`, `casts`, `soft_deletes = True` |
|
|
34
|
+
| `$this->hasMany(Post::class)` | `self.has_many(Post)` |
|
|
35
|
+
| `$this->belongsTo(User::class)` | `self.belongs_to(User)` |
|
|
36
|
+
| `User::paginate(15)` | `User.query().paginate(per_page=15, page=1)` |
|
|
37
|
+
| `Schema::create('t', fn (Blueprint $t) => ...)` | `Schema.create("t", lambda t: [...])` |
|
|
38
|
+
| `$table->string('name')->nullable()` | `t.string("name").nullable()` |
|
|
39
|
+
| `$table->foreignId('user_id')->constrained()` | `t.integer("user_id")` + `t.foreign("user_id").references("id").on("users")` |
|
|
40
|
+
| `Hash::make($p)` / `Hash::check($p, $h)` | `Hash.make(p)` / `Hash.check(p, h)` |
|
|
41
|
+
| `collect($items)->pluck('a')` | `collect(items).pluck("a")` |
|
|
42
|
+
| `session('cart')`, `session(['cart' => 1])` | `request.session.get("cart")`, `request.session.put("cart", 1)` |
|
|
43
|
+
| `redirect()->with('status', 'Saved')` | `request.session.flash("status", "Saved")` |
|
|
44
|
+
| `@csrf` | `{!! csrf_field !!}` |
|
|
45
|
+
| `VerifyCsrfToken` / `$except` | `VerifyCsrfToken` / `except_paths = [...]` |
|
|
46
|
+
| `auth` middleware | `RequireLogin` |
|
|
47
|
+
| `php artisan key:generate` | `python -m manage key:generate` |
|
|
48
|
+
| `$request->file('a')->store('dir')` | `request.file("a").save("dir")` |
|
|
49
|
+
| `'photo' => 'required\|image\|max:2048'` | `"photo": "required\|file\|mimes:png,jpg\|max_kb:2048"` |
|
|
50
|
+
| `View::share('k', $v)` | `app.share("k", v)` |
|
|
51
|
+
| `$this->get('/')->assertStatus(200)` | `client.get("/").status_code == 200` |
|
|
52
|
+
| `app()->bind(...)`, `app()->singleton(...)`, `app()->make(...)` | `app.bind(...)`, `app.singleton(...)`, `app.make(...)` |
|
|
53
|
+
| Service providers `register()` / `boot()` | `ServiceProvider.register()` / `boot()` |
|
|
54
|
+
| `App\Http\Middleware\X` `handle($request, Closure $next)` | `class X(Middleware): def handle(self, request, next)` |
|
|
55
|
+
|
|
56
|
+
## Concepts that differ
|
|
57
|
+
|
|
58
|
+
- **Parameters are named after your function arguments**, and arrive as strings.
|
|
59
|
+
- **No facades**: you import functions and classes directly.
|
|
60
|
+
- **`fillable` is required** for mass assignment (closed by default).
|
|
61
|
+
- **`except` is `except_`** because `except` is a Python keyword.
|
|
62
|
+
- **No Eloquent magic properties for relations**: call the relationship method, `user.posts()`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
PyLara has two configuration mechanisms: **environment variables** (`.env`) for things that
|
|
4
|
+
change between machines, and **JSON config files** in `config/` for everything else.
|
|
5
|
+
|
|
6
|
+
## Environment configuration
|
|
7
|
+
|
|
8
|
+
When the `Application` is created it loads `.env` from the project root (the `base_path`).
|
|
9
|
+
|
|
10
|
+
```dotenv
|
|
11
|
+
APP_NAME="Blog"
|
|
12
|
+
APP_ENV=local
|
|
13
|
+
APP_DEBUG=true
|
|
14
|
+
DB_DATABASE=storage/database.sqlite
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Rules:
|
|
18
|
+
|
|
19
|
+
- Lines starting with `#` are comments. A value may be quoted (`"two words"`). An unquoted value
|
|
20
|
+
may end with ` # comment`.
|
|
21
|
+
- **Real environment variables always win** over `.env`. `DB_DATABASE=:memory: python -m manage migrate`
|
|
22
|
+
overrides the file.
|
|
23
|
+
- Never commit `.env`; commit `.env.example` instead. New projects already contain both, and
|
|
24
|
+
`.env` is in `.gitignore`.
|
|
25
|
+
|
|
26
|
+
### Reading values
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
app.env("APP_NAME", "PyLara") # on the application
|
|
30
|
+
from pylara.support.helpers import env
|
|
31
|
+
env("APP_DEBUG", False) # helper (uses the most recently created application)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`env()` converts the strings `true`, `false`, `null` and `empty` (also `(true)`, `(false)`,
|
|
35
|
+
`(null)`, `(empty)`) into `True`, `False`, `None` and `""`, like Laravel. Everything else is
|
|
36
|
+
returned as a string, so convert numbers yourself: `int(env("WORKERS", 2))`.
|
|
37
|
+
|
|
38
|
+
### Variables PyLara itself understands
|
|
39
|
+
|
|
40
|
+
| Variable | Used by | Meaning |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `APP_DEBUG` | error handling | `true` shows tracebacks in the browser. **Must be `false` in production.** |
|
|
43
|
+
| `DB_DATABASE` | `bootstrap/app.py` (generated project) | SQLite file path, or `:memory:` |
|
|
44
|
+
| `APP_KEY` | [sessions](sessions.md) | secret that signs session cookies. Create it with `python -m manage key:generate`. |
|
|
45
|
+
| `SESSION_DRIVER` | sessions | `file` (default) or `memory` |
|
|
46
|
+
| `SESSION_LIFETIME` | sessions | minutes of inactivity before a session expires (default `120`) |
|
|
47
|
+
| `SESSION_SECURE` | sessions | `true` on HTTPS: the cookie is never sent over plain HTTP |
|
|
48
|
+
| `MAX_BODY_MB` | requests | largest accepted request body in megabytes (default `10`); larger requests get 413 |
|
|
49
|
+
| `APP_NAME`, `APP_ENV` | your code | Free to use. The welcome page shows `APP_NAME`. |
|
|
50
|
+
|
|
51
|
+
Add your own freely (`MAIL_HOST`, `STRIPE_KEY`, ...).
|
|
52
|
+
|
|
53
|
+
## Configuration files
|
|
54
|
+
|
|
55
|
+
Every `config/<file>.json` is loaded on demand and read with **dot notation**: the first segment
|
|
56
|
+
is the file name.
|
|
57
|
+
|
|
58
|
+
`config/app.json`
|
|
59
|
+
```json
|
|
60
|
+
{ "name": "Blog", "per_page": 15, "mail": { "host": "smtp.example.com" } }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
app.config.get("app.per_page") # 15
|
|
65
|
+
app.config.get("app.mail.host") # "smtp.example.com"
|
|
66
|
+
app.config.get("app.missing", "fallback") # "fallback"
|
|
67
|
+
app.config.set("app.per_page", 25) # runtime override (in memory only)
|
|
68
|
+
|
|
69
|
+
from pylara.support.helpers import config
|
|
70
|
+
config("app.name")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
JSON files cannot read the environment. If a value depends on the machine, use `env()` in
|
|
74
|
+
`bootstrap/app.py` or a provider instead.
|
|
75
|
+
|
|
76
|
+
## Debug mode
|
|
77
|
+
|
|
78
|
+
With `APP_DEBUG=true` an unhandled exception renders a page with the traceback, and JSON requests
|
|
79
|
+
receive the exception message. With `APP_DEBUG=false` users only see a generic "Server Error".
|
|
80
|
+
**In both modes the traceback is printed to stderr**, so your server log always has it.
|
|
81
|
+
|
|
82
|
+
## Database configuration
|
|
83
|
+
|
|
84
|
+
The database connection is created in `bootstrap/app.py`; see [Database](database.md).
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Console (Command Line)
|
|
2
|
+
|
|
3
|
+
PyLara has two command line entry points.
|
|
4
|
+
|
|
5
|
+
| Command | Where | Purpose |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `pylara new <name>` | anywhere | create a new application |
|
|
8
|
+
| `python -m manage <command>` | inside a project | everything else (the equivalent of `php artisan`) |
|
|
9
|
+
|
|
10
|
+
`python -m pylara ...` is the same as `pylara ...` and works even when the scripts folder is not on
|
|
11
|
+
your `PATH`.
|
|
12
|
+
|
|
13
|
+
## Built-in commands
|
|
14
|
+
|
|
15
|
+
Run `python -m manage` (or `python -m manage help`) to list them.
|
|
16
|
+
|
|
17
|
+
| Command | Description |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `serve [--host H] [--port P]` | development server (default 127.0.0.1:8000) |
|
|
20
|
+
| `key:generate [--force] [--show]` | create `APP_KEY` in `.env` (signs session cookies) |
|
|
21
|
+
| `route:list` | table of methods, URIs, route names and actions |
|
|
22
|
+
| `make:controller Name [--resource]` | create `app/controllers/<name>.py` |
|
|
23
|
+
| `make:model Name [-m]` | create `app/models/<name>.py` (and a migration with `-m`) |
|
|
24
|
+
| `make:middleware Name` | create `app/middleware/<name>.py` |
|
|
25
|
+
| `make:migration name` | create a timestamped file in `database/migrations/` |
|
|
26
|
+
| `migrate` | run pending migrations |
|
|
27
|
+
| `migrate:status` | list migrations as Ran / Pending |
|
|
28
|
+
| `migrate:rollback [--steps N]` | undo the last N batches (default 1) |
|
|
29
|
+
|
|
30
|
+
Generators refuse to overwrite an existing file, and names must be plain identifiers (letters,
|
|
31
|
+
digits, `_`), so `make:controller ../x` is rejected. Exit codes: `0` success, `1` error.
|
|
32
|
+
|
|
33
|
+
## Custom commands
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# app/commands/greet.py
|
|
37
|
+
from pylara.console.console import Command, CommandError, load_app
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GreetCommand(Command):
|
|
41
|
+
name = "greet"
|
|
42
|
+
description = "Say hello"
|
|
43
|
+
|
|
44
|
+
def configure(self, parser): # an argparse.ArgumentParser
|
|
45
|
+
parser.add_argument("name")
|
|
46
|
+
parser.add_argument("--shout", action="store_true")
|
|
47
|
+
|
|
48
|
+
def handle(self, args):
|
|
49
|
+
if not args.name.strip():
|
|
50
|
+
raise CommandError("Name must not be empty") # prints an error, exit code 1
|
|
51
|
+
text = f"Hello, {args.name}!"
|
|
52
|
+
self.info(text.upper() if args.shout else text)
|
|
53
|
+
return 0
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Register it in `manage.py`:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from app.commands.greet import GreetCommand
|
|
60
|
+
console.register(GreetCommand())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m manage greet Ana --shout
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Output helpers on `Command`: `info()` (green), `warn()` (yellow), `error()` (red, stderr), `line()`.
|
|
68
|
+
Colors are used only on a terminal and are disabled when `NO_COLOR` is set.
|
|
69
|
+
|
|
70
|
+
### Using the application inside a command
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
def handle(self, args):
|
|
74
|
+
app = load_app() # imports bootstrap.app from the current directory
|
|
75
|
+
... # models are ready to use, the database is connected
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`load_app()` raises a friendly `CommandError` outside a project.
|
|
79
|
+
|
|
80
|
+
## Running the console from code
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pylara.console.console import Console
|
|
84
|
+
exit_code = Console().run(["migrate:status"])
|
|
85
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Service Container
|
|
2
|
+
|
|
3
|
+
The service container is a registry that knows how to build objects. The `Application` **is** a
|
|
4
|
+
container, so `app.bind(...)`, `app.make(...)` and friends are all available.
|
|
5
|
+
|
|
6
|
+
## Binding
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
class Mailer:
|
|
10
|
+
def __init__(self, host): self.host = host
|
|
11
|
+
|
|
12
|
+
# A factory that runs every time make() is called
|
|
13
|
+
app.bind("mailer", lambda: Mailer("smtp.example.com"))
|
|
14
|
+
|
|
15
|
+
# A factory that runs once; the result is reused
|
|
16
|
+
app.singleton("mailer", lambda: Mailer("smtp.example.com"))
|
|
17
|
+
|
|
18
|
+
# An object you already built
|
|
19
|
+
app.instance("mailer", Mailer("smtp.example.com"))
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Factory contract.** A factory that accepts a parameter receives the container; a factory with no
|
|
23
|
+
parameters is called bare:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
app.singleton("mailer", lambda app: Mailer(app.env("MAIL_HOST", "localhost")))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You may bind by name (`"mailer"`) or by class. Classes are stored under their class name:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
app.singleton(Mailer, lambda app: Mailer(app.env("MAIL_HOST")))
|
|
33
|
+
app.make(Mailer) # same as app.make("Mailer")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Resolving
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
mailer = app.make("mailer") # KeyError if nothing is bound
|
|
40
|
+
app.bound("mailer") # True / False
|
|
41
|
+
app["mailer"] # shorthand for make()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`app.make("app")`, `app.make("router")` and `app.make("config")` are pre-registered.
|
|
45
|
+
|
|
46
|
+
## Automatic injection
|
|
47
|
+
|
|
48
|
+
`app.build(SomeClass)` instantiates a class and fills its constructor from **type hints**: each
|
|
49
|
+
annotated parameter whose type is bound in the container is resolved. Parameters with a default
|
|
50
|
+
value use it. Anything else raises a `KeyError` that names the parameter.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
class Reports:
|
|
54
|
+
def __init__(self, mailer: Mailer, limit: int = 10):
|
|
55
|
+
self.mailer, self.limit = mailer, limit
|
|
56
|
+
|
|
57
|
+
reports = app.build(Reports)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Controllers get this treatment automatically: the router builds them with `app.build`.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
class ReportController(Controller):
|
|
64
|
+
def __init__(self, mailer: Mailer):
|
|
65
|
+
self.mailer = mailer
|
|
66
|
+
|
|
67
|
+
def send(self, request):
|
|
68
|
+
...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Register `Mailer` in a [service provider](providers.md) and the controller receives it.
|
|
72
|
+
|
|
73
|
+
## Binding is not global state
|
|
74
|
+
|
|
75
|
+
Everything is attached to the application object you create, so tests can build a fresh
|
|
76
|
+
`Application`, bind fakes, and run against it:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
app.instance("mailer", FakeMailer())
|
|
80
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for helping improve PyLara.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone <your fork>
|
|
9
|
+
cd pylara
|
|
10
|
+
python -m venv .venv
|
|
11
|
+
.venv\Scripts\activate # Windows (Linux/macOS: source .venv/bin/activate)
|
|
12
|
+
python -m pip install -e .
|
|
13
|
+
python -m tests
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`python -m tests` runs both suites:
|
|
17
|
+
|
|
18
|
+
- `tests/test_framework.py` is the original script-style suite for the core (ORM, router, request,
|
|
19
|
+
response, middleware, migrations, collections).
|
|
20
|
+
- `tests/test_features.py` is a `unittest` suite for 1.0 features, including integration tests that
|
|
21
|
+
generate a project with `pylara new` and run it.
|
|
22
|
+
|
|
23
|
+
Run one file or test: `python -m unittest tests.test_features.RoutingTest -v`.
|
|
24
|
+
|
|
25
|
+
## Layout
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
pylara/
|
|
29
|
+
├── application.py Application: container, env, static files, WSGI, dev server
|
|
30
|
+
├── cli.py / __main__.py global `pylara` command (pylara new)
|
|
31
|
+
├── version.py the only place the version is written
|
|
32
|
+
├── testing.py TestClient
|
|
33
|
+
├── http/ request, response, kernel (middleware + errors), controller, exceptions
|
|
34
|
+
├── routing/router.py Route, Router
|
|
35
|
+
├── middleware/ Middleware base and built-ins
|
|
36
|
+
├── orm/model.py QueryBuilder and Model
|
|
37
|
+
├── database/migration.py Blueprint, Schema, Migrator, discover
|
|
38
|
+
├── console/console.py manage.py commands
|
|
39
|
+
├── support/ container, config, helpers/Collection, hashing, view engine, providers
|
|
40
|
+
└── stubs/project/ files copied by `pylara new` (*.stub; __UPPER__ tokens are replaced)
|
|
41
|
+
examples/demo/ a small users/posts/auth demo application
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Guidelines
|
|
45
|
+
|
|
46
|
+
- Keep PyLara **dependency-free** (standard library only).
|
|
47
|
+
- Every behaviour change needs a test, and the documentation in `docs/` must stay accurate: if you
|
|
48
|
+
change an API, change its page.
|
|
49
|
+
- Be conservative with security: values are always SQL parameters, column names go through the
|
|
50
|
+
identifier check, HTML output is escaped, static paths cannot leave `public/`.
|
|
51
|
+
- Run the suite with warnings as errors: `python -W error -m tests`.
|
|
52
|
+
- Files in `pylara/stubs/project` end with `.stub` so they are not treated as Python modules when
|
|
53
|
+
the package is installed. New stub files are picked up automatically.
|
|
54
|
+
|
|
55
|
+
## Releasing
|
|
56
|
+
|
|
57
|
+
See [Publishing to PyPI](publishing.md).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Controllers
|
|
2
|
+
|
|
3
|
+
Controllers group request-handling logic into classes.
|
|
4
|
+
|
|
5
|
+
## Generating a controller
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m manage make:controller PostController # one index() method
|
|
9
|
+
python -m manage make:controller PostController --resource # CRUD methods
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
This writes `app/controllers/post_controller.py` and prints the import line to use.
|
|
13
|
+
|
|
14
|
+
## Writing controllers
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from pylara import Controller, Request
|
|
18
|
+
from app.models.post import Post
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PostController(Controller):
|
|
22
|
+
|
|
23
|
+
def index(self, request: Request):
|
|
24
|
+
return self.json([p.to_dict() for p in Post.all()])
|
|
25
|
+
|
|
26
|
+
def show(self, request: Request, id: str):
|
|
27
|
+
return self.json(Post.find_or_fail(int(id)).to_dict())
|
|
28
|
+
|
|
29
|
+
def store(self, request: Request):
|
|
30
|
+
data = request.validate({"title": "required|string|max:150", "body": "required"})
|
|
31
|
+
return self.json(Post.create(**data).to_dict(), 201)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Every action receives the `request` first, followed by the route parameters it declares (by
|
|
35
|
+
name). Route them:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
app.get("/posts/{id}", [PostController, "show"])
|
|
39
|
+
app.resource("posts", PostController)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## What you can return
|
|
43
|
+
|
|
44
|
+
A `Response`, a `dict`/`list` (JSON), a `str` (HTML) or `None`. See
|
|
45
|
+
[Responses](responses.md).
|
|
46
|
+
|
|
47
|
+
## Helpers on the base class
|
|
48
|
+
|
|
49
|
+
| Method | Does |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `self.json(data, status=200)` | JSON response |
|
|
52
|
+
| `self.view(name, data=None, status=200, **context)` | render a [view](views.md) |
|
|
53
|
+
| `self.response(content="", status=200)` | plain response |
|
|
54
|
+
| `self.redirect(url, status=302)` | redirect |
|
|
55
|
+
| `self.abort(status, message="")` | stop with an HTTP error |
|
|
56
|
+
| `self.not_found(message="Not Found")` | `abort(404)` |
|
|
57
|
+
| `self.unauthorized(message="Unauthorized")` | `abort(401)` |
|
|
58
|
+
|
|
59
|
+
## Dependency injection
|
|
60
|
+
|
|
61
|
+
Controllers are created through the [container](container.md), so type-hinted constructor
|
|
62
|
+
parameters that are bound in the container are injected:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
class ReportController(Controller):
|
|
66
|
+
def __init__(self, mailer: Mailer):
|
|
67
|
+
self.mailer = mailer
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A new controller instance is built **for every request**.
|
|
71
|
+
|
|
72
|
+
## Resource controllers
|
|
73
|
+
|
|
74
|
+
Implement any of `index create store show edit update destroy` and call `app.resource(...)`. See
|
|
75
|
+
[Routing](routing.md#resource-routes). The controller may implement only the methods it needs.
|
|
76
|
+
|
|
77
|
+
## Single-action routes
|
|
78
|
+
|
|
79
|
+
Not every endpoint needs a class: a function or lambda is a perfectly good action.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
@app.post("/subscribe")
|
|
83
|
+
def subscribe(request):
|
|
84
|
+
data = request.validate({"email": "required|email"})
|
|
85
|
+
...
|
|
86
|
+
return {"subscribed": data["email"]}
|
|
87
|
+
```
|