bingo-framework 0.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 (111) hide show
  1. bingo_framework-0.2.0/.gitignore +176 -0
  2. bingo_framework-0.2.0/LICENSE +21 -0
  3. bingo_framework-0.2.0/PKG-INFO +414 -0
  4. bingo_framework-0.2.0/README.md +385 -0
  5. bingo_framework-0.2.0/bingo/__init__.py +44 -0
  6. bingo_framework-0.2.0/bingo/application.py +136 -0
  7. bingo_framework-0.2.0/bingo/channel_backends.py +163 -0
  8. bingo_framework-0.2.0/bingo/channels.js +145 -0
  9. bingo_framework-0.2.0/bingo/channels.py +566 -0
  10. bingo_framework-0.2.0/bingo/cli/__init__.py +3 -0
  11. bingo_framework-0.2.0/bingo/cli/app.py +32 -0
  12. bingo_framework-0.2.0/bingo/cli/generate.py +64 -0
  13. bingo_framework-0.2.0/bingo/cli/helpers.py +39 -0
  14. bingo_framework-0.2.0/bingo/cli/inspect.py +17 -0
  15. bingo_framework-0.2.0/bingo/cli/migrate.py +26 -0
  16. bingo_framework-0.2.0/bingo/cli/new.py +20 -0
  17. bingo_framework-0.2.0/bingo/cli/routes.py +12 -0
  18. bingo_framework-0.2.0/bingo/cli/server.py +45 -0
  19. bingo_framework-0.2.0/bingo/cli/worker.py +25 -0
  20. bingo_framework-0.2.0/bingo/controller.py +47 -0
  21. bingo_framework-0.2.0/bingo/conventions/__init__.py +4 -0
  22. bingo_framework-0.2.0/bingo/conventions/errors.py +18 -0
  23. bingo_framework-0.2.0/bingo/conventions/inspector.py +773 -0
  24. bingo_framework-0.2.0/bingo/db/__init__.py +7 -0
  25. bingo_framework-0.2.0/bingo/db/database.py +46 -0
  26. bingo_framework-0.2.0/bingo/db/fields.py +67 -0
  27. bingo_framework-0.2.0/bingo/db/migration.py +171 -0
  28. bingo_framework-0.2.0/bingo/db/model.py +100 -0
  29. bingo_framework-0.2.0/bingo/db/naming.py +24 -0
  30. bingo_framework-0.2.0/bingo/db/query.py +81 -0
  31. bingo_framework-0.2.0/bingo/exceptions.py +45 -0
  32. bingo_framework-0.2.0/bingo/forms/__init__.py +3 -0
  33. bingo_framework-0.2.0/bingo/forms/form.py +144 -0
  34. bingo_framework-0.2.0/bingo/generators/__init__.py +11 -0
  35. bingo_framework-0.2.0/bingo/generators/channel.py +75 -0
  36. bingo_framework-0.2.0/bingo/generators/project.py +495 -0
  37. bingo_framework-0.2.0/bingo/generators/resource.py +407 -0
  38. bingo_framework-0.2.0/bingo/generators/task.py +46 -0
  39. bingo_framework-0.2.0/bingo/management.py +147 -0
  40. bingo_framework-0.2.0/bingo/request.py +71 -0
  41. bingo_framework-0.2.0/bingo/response.py +3 -0
  42. bingo_framework-0.2.0/bingo/routing.py +323 -0
  43. bingo_framework-0.2.0/bingo/settings.py +105 -0
  44. bingo_framework-0.2.0/bingo/tasks.py +345 -0
  45. bingo_framework-0.2.0/bingo/templates/__init__.py +3 -0
  46. bingo_framework-0.2.0/bingo/templates/engine.py +175 -0
  47. bingo_framework-0.2.0/bingo/validation/__init__.py +4 -0
  48. bingo_framework-0.2.0/bingo/validation/rules.py +131 -0
  49. bingo_framework-0.2.0/bingo/validation/validator.py +52 -0
  50. bingo_framework-0.2.0/examples/blog/.env.example +4 -0
  51. bingo_framework-0.2.0/examples/blog/.gitignore +5 -0
  52. bingo_framework-0.2.0/examples/blog/BINGO.md +83 -0
  53. bingo_framework-0.2.0/examples/blog/README.md +59 -0
  54. bingo_framework-0.2.0/examples/blog/app/__init__.py +0 -0
  55. bingo_framework-0.2.0/examples/blog/app/channels/__init__.py +0 -0
  56. bingo_framework-0.2.0/examples/blog/app/channels/application_channel.py +5 -0
  57. bingo_framework-0.2.0/examples/blog/app/channels/application_connection.py +5 -0
  58. bingo_framework-0.2.0/examples/blog/app/channels/chat_channel.py +17 -0
  59. bingo_framework-0.2.0/examples/blog/app/commands/__init__.py +0 -0
  60. bingo_framework-0.2.0/examples/blog/app/commands/hello_hirak.py +8 -0
  61. bingo_framework-0.2.0/examples/blog/app/controllers/__init__.py +0 -0
  62. bingo_framework-0.2.0/examples/blog/app/controllers/application_controller.py +5 -0
  63. bingo_framework-0.2.0/examples/blog/app/controllers/chat_controller.py +6 -0
  64. bingo_framework-0.2.0/examples/blog/app/controllers/posts_controller.py +63 -0
  65. bingo_framework-0.2.0/examples/blog/app/models/__init__.py +0 -0
  66. bingo_framework-0.2.0/examples/blog/app/models/post.py +9 -0
  67. bingo_framework-0.2.0/examples/blog/app/tasks/__init__.py +0 -0
  68. bingo_framework-0.2.0/examples/blog/app/tasks/application_task.py +5 -0
  69. bingo_framework-0.2.0/examples/blog/app/validators/__init__.py +0 -0
  70. bingo_framework-0.2.0/examples/blog/app/validators/chat_message_validator.py +6 -0
  71. bingo_framework-0.2.0/examples/blog/app/validators/post_create_validator.py +7 -0
  72. bingo_framework-0.2.0/examples/blog/app/validators/post_update_validator.py +7 -0
  73. bingo_framework-0.2.0/examples/blog/app/views/channels/chat/message.bjson +4 -0
  74. bingo_framework-0.2.0/examples/blog/app/views/chat/show.html +27 -0
  75. bingo_framework-0.2.0/examples/blog/app/views/layouts/application.html +12 -0
  76. bingo_framework-0.2.0/examples/blog/app/views/posts/edit.html +15 -0
  77. bingo_framework-0.2.0/examples/blog/app/views/posts/index.bjson +13 -0
  78. bingo_framework-0.2.0/examples/blog/app/views/posts/index.html +12 -0
  79. bingo_framework-0.2.0/examples/blog/app/views/posts/new.html +14 -0
  80. bingo_framework-0.2.0/examples/blog/app/views/posts/show.bjson +10 -0
  81. bingo_framework-0.2.0/examples/blog/app/views/posts/show.html +18 -0
  82. bingo_framework-0.2.0/examples/blog/compose.yaml +16 -0
  83. bingo_framework-0.2.0/examples/blog/config/__init__.py +0 -0
  84. bingo_framework-0.2.0/examples/blog/config/application.py +15 -0
  85. bingo_framework-0.2.0/examples/blog/config/routes.py +6 -0
  86. bingo_framework-0.2.0/examples/blog/config/settings/__init__.py +0 -0
  87. bingo_framework-0.2.0/examples/blog/config/settings/base.py +25 -0
  88. bingo_framework-0.2.0/examples/blog/config/settings/development.py +14 -0
  89. bingo_framework-0.2.0/examples/blog/config/settings/production.py +8 -0
  90. bingo_framework-0.2.0/examples/blog/config/settings/test.py +5 -0
  91. bingo_framework-0.2.0/examples/blog/db/development.sqlite3 +0 -0
  92. bingo_framework-0.2.0/examples/blog/db/migrations/20260906064756730305_create_posts.py +15 -0
  93. bingo_framework-0.2.0/examples/blog/manage.py +4 -0
  94. bingo_framework-0.2.0/examples/blog/public/application.css +90 -0
  95. bingo_framework-0.2.0/examples/blog/public/chat.js +48 -0
  96. bingo_framework-0.2.0/examples/blog/pyproject.toml +13 -0
  97. bingo_framework-0.2.0/examples/blog/tests/test_chat.py +38 -0
  98. bingo_framework-0.2.0/examples/blog/tests/test_posts.py +10 -0
  99. bingo_framework-0.2.0/pyproject.toml +51 -0
  100. bingo_framework-0.2.0/tests/conftest.py +17 -0
  101. bingo_framework-0.2.0/tests/test_channels.py +146 -0
  102. bingo_framework-0.2.0/tests/test_database_and_migrations.py +72 -0
  103. bingo_framework-0.2.0/tests/test_generated_blog_integration.py +153 -0
  104. bingo_framework-0.2.0/tests/test_generators_and_inspector.py +224 -0
  105. bingo_framework-0.2.0/tests/test_naming.py +8 -0
  106. bingo_framework-0.2.0/tests/test_routing_and_controllers.py +294 -0
  107. bingo_framework-0.2.0/tests/test_server.py +48 -0
  108. bingo_framework-0.2.0/tests/test_settings_and_commands.py +65 -0
  109. bingo_framework-0.2.0/tests/test_tasks.py +164 -0
  110. bingo_framework-0.2.0/tests/test_validation_and_forms.py +99 -0
  111. bingo_framework-0.2.0/tests/test_views.py +94 -0
@@ -0,0 +1,176 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python
3
+
4
+ ### Python ###
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # Pyre type checker
151
+ .pyre/
152
+
153
+ # pytype static type analyzer
154
+ .pytype/
155
+
156
+ # Cython debug symbols
157
+ cython_debug/
158
+
159
+ # PyCharm
160
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
163
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164
+ #.idea/
165
+
166
+ ### Python Patch ###
167
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168
+ poetry.toml
169
+
170
+ # ruff
171
+ .ruff_cache/
172
+
173
+ # LSP config files
174
+ pyrightconfig.json
175
+
176
+ # End of https://www.toptal.com/developers/gitignore/api/python
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bingo contributors
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.
@@ -0,0 +1,414 @@
1
+ Metadata-Version: 2.5
2
+ Name: bingo-framework
3
+ Version: 0.2.0
4
+ Summary: The opinionated Python web framework for the AI era.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: aiosqlite>=0.20
9
+ Requires-Dist: alembic>=1.13
10
+ Requires-Dist: granian[reload]<3,>=2.8
11
+ Requires-Dist: itsdangerous>=2.2
12
+ Requires-Dist: jinja2>=3.1
13
+ Requires-Dist: pydantic>=2.8
14
+ Requires-Dist: python-multipart>=0.0.9
15
+ Requires-Dist: rich>=13.7
16
+ Requires-Dist: saq[redis]<0.27,>=0.26
17
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
18
+ Requires-Dist: starlette>=0.38
19
+ Requires-Dist: typer>=0.12
20
+ Provides-Extra: postgres
21
+ Requires-Dist: psycopg[binary]<4,>=3.2; extra == 'postgres'
22
+ Requires-Dist: saq[postgres]<0.27,>=0.26; extra == 'postgres'
23
+ Provides-Extra: test
24
+ Requires-Dist: httpx>=0.27; extra == 'test'
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
26
+ Requires-Dist: pytest>=8.2; extra == 'test'
27
+ Requires-Dist: ruff>=0.6; extra == 'test'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Bingo
31
+
32
+ **The opinionated Python web framework for the AI era.**
33
+
34
+ Bingo 0.2 provides one conventional path from a new project to a complete async
35
+ CRUD application:
36
+
37
+ ```bash
38
+ uv tool install .
39
+ bingo new blog
40
+ cd blog
41
+ python manage.py generate resource Post title:string body:text published:boolean
42
+ python manage.py migrate
43
+ python manage.py server
44
+ ```
45
+
46
+ To initialize the current directory instead of creating a child directory:
47
+
48
+ ```bash
49
+ mkdir blog
50
+ cd blog
51
+ bingo new .
52
+ ```
53
+
54
+ Bingo derives the application name from the current directory. Unrelated files
55
+ are preserved. If a generated path already exists, Bingo reports every conflict
56
+ before writing anything.
57
+
58
+ New applications include a welcome controller and view at `/`, plus a small
59
+ stylesheet in `public/`, so `python manage.py server` immediately opens a working page.
60
+ Granian is Bingo's ASGI server and serves `/public` directly without sending
61
+ static-file requests through Python.
62
+
63
+ Applications use class controllers, routes in `config/routes.py`, async models,
64
+ standalone validators, Jinja templates, and Bingo migrations. Generated projects
65
+ include `BINGO.md` with concise architectural rules for coding agents.
66
+
67
+ ## Settings
68
+
69
+ Settings are ordinary uppercase Python values split by environment:
70
+
71
+ ```text
72
+ config/settings/base.py
73
+ config/settings/development.py
74
+ config/settings/test.py
75
+ config/settings/production.py
76
+ ```
77
+
78
+ Bingo loads `base.py` and overlays the environment selected by `BINGO_ENV`. It
79
+ defaults to `development`. Application code accesses both framework and custom
80
+ settings through one object:
81
+
82
+ ```python
83
+ from bingo import settings
84
+
85
+ page_size = settings.POSTS_PER_PAGE
86
+ ```
87
+
88
+ Environment files never import the base file. Bingo performs the merge, so each
89
+ file contains only plain settings and environment-specific overrides.
90
+
91
+ ## Application commands
92
+
93
+ Inside a project, `manage.py` is the only command entry point. Application-owned
94
+ commands live directly in `app/commands/`:
95
+
96
+ ```python
97
+ from bingo import BaseCommand
98
+
99
+
100
+ class Command(BaseCommand):
101
+ help = "Publish pending posts."
102
+
103
+ async def handle(self, limit: int = 10, dry_run: bool = False): ...
104
+ ```
105
+
106
+ Run it using its filename:
107
+
108
+ ```bash
109
+ python manage.py publish_posts --limit 50 --dry-run
110
+ ```
111
+
112
+ Required parameters become positional arguments. Parameters with defaults become
113
+ options, and boolean parameters become flags. `python manage.py --help` lists
114
+ built-in and application commands.
115
+
116
+ ## Background tasks
117
+
118
+ Tasks use one application-facing API while SAQ handles durable queue mechanics
119
+ underneath. Generate a task with:
120
+
121
+ ```bash
122
+ python manage.py generate task SendWelcomeEmail
123
+ ```
124
+
125
+ This creates `app/tasks/send_welcome_email_task.py`:
126
+
127
+ ```python
128
+ from app.tasks.application_task import ApplicationTask
129
+
130
+
131
+ class SendWelcomeEmailTask(ApplicationTask):
132
+ queue = "mailers"
133
+ retries = 3
134
+ timeout = 60
135
+
136
+ async def run(self, user_id: int):
137
+ user = await User.find_or_fail(user_id)
138
+ await send_email(user.email)
139
+ ```
140
+
141
+ Controllers and commands enqueue it through the class:
142
+
143
+ ```python
144
+ await SendWelcomeEmailTask.enqueue(user.id)
145
+ ```
146
+
147
+ Run a worker for the default or a named queue:
148
+
149
+ ```bash
150
+ python manage.py worker
151
+ python manage.py worker --queue mailers
152
+ ```
153
+
154
+ `TASK_QUEUE_URL` selects the backend. Bingo defaults to Redis:
155
+
156
+ ```python
157
+ TASK_QUEUE_URL = "redis://localhost:6379/0"
158
+ ```
159
+
160
+ PostgreSQL is also supported without changing task code. Install
161
+ `bingo-framework[postgres]` and use a `postgres://` or `postgresql://` URL.
162
+
163
+ Task arguments and return values must be JSON-compatible. Pass model IDs instead
164
+ of model instances, and make tasks safe to execute more than once because durable
165
+ queues provide at-least-once delivery. `config/settings/test.py` sets
166
+ `TASKS_INLINE = True`, so tests execute tasks immediately without a queue server.
167
+
168
+ ## Realtime channels
169
+
170
+ Channels provide ephemeral WebSocket updates through one multiplexed endpoint at
171
+ `/channels`. Generate a channel and its event views together:
172
+
173
+ ```bash
174
+ python manage.py generate channel Chat message presence
175
+ ```
176
+
177
+ The generated `app/channels/chat_channel.py` owns subscription and incoming
178
+ message behavior:
179
+
180
+ ```python
181
+ from app.channels.application_channel import ApplicationChannel
182
+
183
+
184
+ class ChatChannel(ApplicationChannel):
185
+ async def subscribed(self):
186
+ await self.stream(self.params["room"])
187
+
188
+ async def received(self, data: dict):
189
+ message = ChatMessageValidator(data).validate()
190
+ await type(self).broadcast(
191
+ self.params["room"],
192
+ "message",
193
+ **message,
194
+ )
195
+ ```
196
+
197
+ Broadcasts render `app/views/channels/chat/message.bjson`, so channels never
198
+ serialize models implicitly. Validator failures are transmitted as the
199
+ `validation_error` event using the same error shape as JSON APIs.
200
+
201
+ Bingo serves its browser client at `/channels.js`; it maintains one WebSocket,
202
+ multiplexes subscriptions, and reconnects automatically:
203
+
204
+ ```html
205
+ <script src="/channels.js"></script>
206
+ <script>
207
+ const chat = Bingo.channels.subscribe("ChatChannel", { room: "lobby" }, {
208
+ connected() { console.log("connected") },
209
+ received(event, data) { console.log(event, data) },
210
+ disconnected() { console.log("reconnecting") },
211
+ })
212
+
213
+ chat.send({ name: "Ada", body: "Hello" })
214
+ </script>
215
+ ```
216
+
217
+ `ApplicationConnection` is the single connection-level hook for attaching
218
+ application state or rejecting a socket. Bingo supplies no user model or default
219
+ authentication policy.
220
+
221
+ Channels and tasks may share infrastructure without sharing semantics:
222
+
223
+ ```python
224
+ CHANNEL_URL = TASK_QUEUE_URL
225
+ ```
226
+
227
+ Redis uses Pub/Sub. PostgreSQL `LISTEN/NOTIFY` remains available through the
228
+ `bingo-framework[postgres]` extra. Tests use `memory://`.
229
+ Broadcasts are online-only, so persist anything clients must retrieve after
230
+ reconnecting. Configure additional browser origins with
231
+ `CHANNEL_ALLOWED_ORIGINS`; same-origin connections are accepted automatically.
232
+
233
+ ## The application shape
234
+
235
+ Routes have one home and one syntax:
236
+
237
+ ```python
238
+ from bingo import Router
239
+
240
+ routes = Router()
241
+ routes.resources("/posts")
242
+ routes.get("/posts/published", "PostsController.published")
243
+ ```
244
+
245
+ Resource controllers are inferred from the resource path:
246
+
247
+ ```text
248
+ /posts → app/controllers/posts_controller.py → PostsController
249
+ ```
250
+
251
+ Custom routes use a `"Controller.action"` target and an explicit HTTP verb.
252
+ Controllers are imported lazily when the application boots, after `config/routes.py`
253
+ has finished loading, so route files do not import application controllers.
254
+
255
+ Groups prefix both the URL and controller directory:
256
+
257
+ ```python
258
+ with routes.group("/admin"):
259
+ routes.resources("/posts")
260
+ routes.get("/posts/published", "PostsController.published")
261
+ ```
262
+
263
+ ```text
264
+ /admin/posts
265
+ → app/controllers/admin/posts_controller.py
266
+ → PostsController
267
+ → app/views/admin/posts/
268
+ ```
269
+
270
+ The directory is the group, so controller classes remain short. Group blocks can
271
+ nest, and every group name uses the same slash-prefixed path syntax:
272
+
273
+ ```python
274
+ with routes.group("/admin"):
275
+ with routes.group("/reports"):
276
+ routes.resources("/sales")
277
+ ```
278
+
279
+ This resolves `SalesController` from
280
+ `app/controllers/admin/reports/sales_controller.py` and serves it under
281
+ `/admin/reports/sales`.
282
+
283
+ Resource controllers provide the seven actions `index`, `show`, `new`, `create`,
284
+ `edit`, `update`, and `destroy`. They receive `self.request`, `self.params`,
285
+ `self.query`, and `self.session`, and return `self.render(...)`,
286
+ `self.redirect(...)`, or `self.json(...)`.
287
+
288
+ HTML and JSON are representations of the same resource actions:
289
+
290
+ ```text
291
+ GET /posts → app/views/posts/index.html
292
+ GET /posts.json → app/views/posts/index.bjson
293
+ GET /posts/1 → app/views/posts/show.html
294
+ GET /posts/1.json → app/views/posts/show.bjson
295
+ ```
296
+
297
+ An extensionless render target follows the URL format:
298
+
299
+ ```python
300
+ return self.render("posts/show", post=post)
301
+ ```
302
+
303
+ Adding `.html` or `.json` to the render target locks it to that format. A format
304
+ mismatch returns 404. HTML views use Jinja. A `.bjson` view contains one restricted
305
+ Python expression that explicitly selects JSON fields; it cannot call functions,
306
+ await work, query models, or execute statements.
307
+
308
+ Every controller has one async `before_action` lifecycle hook. It returns `None`
309
+ to continue or a response to stop dispatch. Application-owned base controllers
310
+ can use this hook for shared policy without middleware registries or decorators.
311
+
312
+ Models and validators stay separate:
313
+
314
+ ```python
315
+ from bingo.db import Model, fields
316
+ from bingo.validation import Validator, rules
317
+
318
+
319
+ class Post(Model):
320
+ title = fields.String(max_length=200)
321
+ published = fields.Boolean(default=False)
322
+
323
+
324
+ class PostCreateValidator(Validator):
325
+ title = rules.String(required=True, max_length=200)
326
+ published = rules.Boolean(required=True)
327
+ ```
328
+
329
+ Validation has one execution path. `validate()` returns cleaned values or raises a
330
+ structured validation error:
331
+
332
+ ```python
333
+ async def create(self):
334
+ data = PostCreateValidator(self.request.data).validate()
335
+ post = await Post.create(**data)
336
+
337
+ if self.request.format == "json":
338
+ return self.render("posts/show", post=post, status=201)
339
+ return self.redirect(f"/posts/{post.id}")
340
+ ```
341
+
342
+ Bingo handles the error at the controller boundary. A JSON request receives
343
+ `{"errors": {"title": ["is required"]}}` with status 422. An HTML `create` or
344
+ `update` request re-runs the conventional `new` or `edit` action with
345
+ `self.validation` set to the invalid validator, also with status 422.
346
+
347
+ HTML forms are generated from those same validator rules:
348
+
349
+ ```python
350
+ async def new(self):
351
+ validator = self.validation or PostCreateValidator()
352
+ return self.render("posts/new.html", validator=validator)
353
+ ```
354
+
355
+ ```jinja
356
+ {% for field in form(validator) %}
357
+ <label for="{{ field.name }}">{{ field.label }}</label>
358
+ {{ field }}
359
+ {{ field.errors }}
360
+ {% endfor %}
361
+ ```
362
+
363
+ The field rule determines the standard HTML control. Use `rules.Text` for a
364
+ textarea; string, email, boolean, numeric, date, and datetime rules select their
365
+ matching inputs. Submitted values and field errors remain on the validator, so a
366
+ failed HTML form is rendered without rebuilding its state.
367
+
368
+ All database operations are async:
369
+
370
+ ```python
371
+ post = await Post.create(title="Hello")
372
+ post = await Post.find_or_fail(1)
373
+ posts = await Post.where(published=True).order_by("-created_at").limit(10).all()
374
+ post.fill(title="Updated")
375
+ await post.save()
376
+ await post.delete()
377
+ ```
378
+
379
+ ## Commands
380
+
381
+ ```text
382
+ bingo new NAME # use . to initialize the current directory
383
+ python manage.py server
384
+ python manage.py generate resource NAME [fields...]
385
+ python manage.py generate model NAME [fields...]
386
+ python manage.py generate controller NAME
387
+ python manage.py generate validator NAME [fields...]
388
+ python manage.py generate task NAME
389
+ python manage.py generate channel NAME [events...]
390
+ python manage.py migrate
391
+ python manage.py rollback
392
+ python manage.py routes
393
+ python manage.py inspect
394
+ python manage.py worker [--queue QUEUE] [--concurrency INTEGER]
395
+ ```
396
+
397
+ Development settings use SQLite by default and accept `DATABASE_URL` for another
398
+ SQLAlchemy async database URL. `python manage.py inspect` reports convention
399
+ violations with the problem, expected structure, and a suggested fix.
400
+
401
+ `python manage.py server` maps the project's `public/` directory to `/public` through
402
+ Granian. Application routes do not need a static-files route or controller.
403
+
404
+ See [`examples/blog`](examples/blog) for a complete generated Post resource.
405
+
406
+ ## Framework development
407
+
408
+ ```bash
409
+ python -m venv .venv
410
+ .venv/bin/pip install -e '.[test]'
411
+ .venv/bin/pytest
412
+ ```
413
+
414
+ Python 3.12 or newer is required.