bitpoint 0.1.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.
- bitpoint-0.1.0/.github/workflows/publish.yml +22 -0
- bitpoint-0.1.0/.gitignore +9 -0
- bitpoint-0.1.0/.pre-commit-config.yaml +7 -0
- bitpoint-0.1.0/LICENSE +21 -0
- bitpoint-0.1.0/PKG-INFO +299 -0
- bitpoint-0.1.0/README.md +272 -0
- bitpoint-0.1.0/bitpoint/__init__.py +8 -0
- bitpoint-0.1.0/bitpoint/__main__.py +108 -0
- bitpoint-0.1.0/bitpoint/app.py +158 -0
- bitpoint-0.1.0/bitpoint/deps.py +118 -0
- bitpoint-0.1.0/bitpoint/loader.py +89 -0
- bitpoint-0.1.0/bitpoint/request.py +60 -0
- bitpoint-0.1.0/bitpoint/response.py +43 -0
- bitpoint-0.1.0/bitpoint/router.py +91 -0
- bitpoint-0.1.0/bitpoint/worker.py +89 -0
- bitpoint-0.1.0/bitpoint/workers.py +125 -0
- bitpoint-0.1.0/pyproject.toml +42 -0
- bitpoint-0.1.0/tests/test_bitpoint.py +368 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: astral-sh/setup-uv@v6
|
|
17
|
+
|
|
18
|
+
- name: Build distributions
|
|
19
|
+
run: uv build
|
|
20
|
+
|
|
21
|
+
- name: Publish to PyPI
|
|
22
|
+
run: uv publish --trusted-publishing always
|
bitpoint-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andros Fenollosa
|
|
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.
|
bitpoint-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bitpoint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Create HTTP endpoints quickly using files, without going through a framework.
|
|
5
|
+
Project-URL: Homepage, https://github.com/tanrax/bitpoint
|
|
6
|
+
Project-URL: Repository, https://github.com/tanrax/bitpoint
|
|
7
|
+
Project-URL: Issues, https://github.com/tanrax/bitpoint/issues
|
|
8
|
+
Author-email: Andros Fenollosa <andros@fenollosa.email>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: endpoints,file-based-routing,http,microframework,pep723,webhooks
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
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 :: HTTP Servers
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: flask>=3.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Bitpoint
|
|
29
|
+
|
|
30
|
+
Create HTTP endpoints quickly using files, without going through a framework like Flask or FastAPI.
|
|
31
|
+
|
|
32
|
+
- **File-based routing**: the directory structure defines the routes, the file name defines the HTTP method.
|
|
33
|
+
- **Hot reload**: save the file and the server updates automatically. Deploy with a simple `git pull`.
|
|
34
|
+
- **Per-endpoint dependencies**: each endpoint declares its dependencies with [PEP 723](https://peps.python.org/pep-0723/) and they are installed automatically in an isolated environment.
|
|
35
|
+
|
|
36
|
+
Bitpoint is for the moments when a full project feels like too much: exposing a script as a webhook, mocking an API so the frontend can move forward, publishing a small internal service for your team, or keeping a handful of endpoints alive on a personal server. In all these cases the API is not the product, it is just plumbing, and spending an afternoon on scaffolding, virtual environments and deploy pipelines is out of proportion. With Bitpoint you write one file and you get one endpoint.
|
|
37
|
+
|
|
38
|
+
Under the hood, Bitpoint stays out of your way. There is nothing to register and nothing to configure: no app object, no decorators, no config files, just a `handle` function per file. Dependency isolation is delegated to [uv](https://docs.astral.sh/uv/), with a cached environment per endpoint, so two endpoints can use incompatible versions of the same library without ever conflicting. Responses follow the type you return: a string becomes plain text, a dict becomes JSON, a tuple adds a status code and headers, and the sensible thing happens by default.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install bitpoint
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or, if you use [uv](https://docs.astral.sh/uv/):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv tool install bitpoint
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Python 3.9 or later. If you want per-endpoint dependencies, `uv` must be available on the server.
|
|
53
|
+
|
|
54
|
+
## Quickstart
|
|
55
|
+
|
|
56
|
+
Create a directory called `routes` and, inside it, a subdirectory called `hello` (this will be the route). Inside `hello`, create a file called `get.py` with the following content:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# routes/hello/get.py
|
|
60
|
+
|
|
61
|
+
def handle(request):
|
|
62
|
+
return "world"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Start the server with:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m bitpoint
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
You can now try your endpoint:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
curl localhost:8000/hello
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
It will return:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
world
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Routing
|
|
84
|
+
|
|
85
|
+
The URL path maps to the directory path inside `routes`, and the HTTP method maps to the file name:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
routes/
|
|
89
|
+
├── hello/
|
|
90
|
+
│ └── get.py → GET /hello
|
|
91
|
+
├── users/
|
|
92
|
+
│ ├── get.py → GET /users
|
|
93
|
+
│ ├── post.py → POST /users
|
|
94
|
+
│ └── [id]/
|
|
95
|
+
│ ├── get.py → GET /users/42
|
|
96
|
+
│ └── delete.py → DELETE /users/42
|
|
97
|
+
└── lib/ → shared code, does not generate routes
|
|
98
|
+
└── db.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Valid file names are the HTTP methods in lowercase: `get.py`, `post.py`, `put.py`, `patch.py`, `delete.py`, `head.py` and `options.py`. Any other file is ignored and does not generate routes, so you can keep helper modules next to your endpoints.
|
|
102
|
+
|
|
103
|
+
### Dynamic routes
|
|
104
|
+
|
|
105
|
+
A directory whose name is wrapped in brackets captures a URL segment:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# routes/users/[id]/get.py
|
|
109
|
+
|
|
110
|
+
def handle(request):
|
|
111
|
+
return {"user_id": request.params["id"]}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
curl localhost:8000/users/42
|
|
116
|
+
# {"user_id": "42"}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Captured values always arrive as `str`. Type conversion is the endpoint's responsibility.
|
|
120
|
+
|
|
121
|
+
## The `handle` contract
|
|
122
|
+
|
|
123
|
+
Each endpoint exposes a `handle(request)` function. It is the only symbol Bitpoint looks for in the file, the rest of the module is yours.
|
|
124
|
+
|
|
125
|
+
### The `request` object
|
|
126
|
+
|
|
127
|
+
| Attribute | Type | Description |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `request.method` | `str` | HTTP method in uppercase, for example `"GET"` |
|
|
130
|
+
| `request.path` | `str` | Request path, for example `/users/42` |
|
|
131
|
+
| `request.params` | `dict[str, str]` | Dynamic path segments |
|
|
132
|
+
| `request.args` | `dict[str, str]` | Query string parameters |
|
|
133
|
+
| `request.headers` | `dict[str, str]` | Headers, case-insensitive keys |
|
|
134
|
+
| `request.body` | `bytes` | Raw request body |
|
|
135
|
+
| `request.json()` | `Any` | Body parsed as JSON, raises a 400 error if invalid |
|
|
136
|
+
|
|
137
|
+
Example with a query string:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# routes/greet/get.py
|
|
141
|
+
|
|
142
|
+
def handle(request):
|
|
143
|
+
name = request.args.get("name", "world")
|
|
144
|
+
return f"Hello, {name}!"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
curl "localhost:8000/greet?name=Bob"
|
|
149
|
+
# Hello, Bob!
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Return values
|
|
153
|
+
|
|
154
|
+
The return type determines the response:
|
|
155
|
+
|
|
156
|
+
| Return | Response |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `str` | `200`, `text/plain; charset=utf-8` |
|
|
159
|
+
| `dict` or `list` | `200`, `application/json` |
|
|
160
|
+
| `bytes` | `200`, `application/octet-stream` |
|
|
161
|
+
| `(body, status)` | As above, with the given status code |
|
|
162
|
+
| `(body, status, headers)` | Additionally with custom headers |
|
|
163
|
+
| `None` | `204 No Content` |
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# routes/users/post.py
|
|
167
|
+
|
|
168
|
+
def handle(request):
|
|
169
|
+
data = request.json()
|
|
170
|
+
return {"created": data["name"]}, 201
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Errors
|
|
174
|
+
|
|
175
|
+
- If `handle` raises an exception, Bitpoint responds with `500`.
|
|
176
|
+
- In development mode (the default) the body includes the traceback. In production (`--production`) the body is a generic message and the traceback goes to the log.
|
|
177
|
+
- Path with no matching directory: `404`. Directory exists but there is no file for that method: `405` with the `Allow` header listing the available methods.
|
|
178
|
+
|
|
179
|
+
## Dependencies
|
|
180
|
+
|
|
181
|
+
Declare each endpoint's dependencies with a [PEP 723](https://peps.python.org/pep-0723/) block at the top of the file, the same format understood by `uv` and `pipx`:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# routes/status/get.py
|
|
185
|
+
|
|
186
|
+
# /// script
|
|
187
|
+
# dependencies = ["requests"]
|
|
188
|
+
# ///
|
|
189
|
+
|
|
190
|
+
def handle(request):
|
|
191
|
+
import requests
|
|
192
|
+
return requests.get("https://example.com").json()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Bitpoint delegates resolution and installation to [uv](https://docs.astral.sh/uv/): each endpoint runs with its dependencies in an isolated, cached environment. Two endpoints can use incompatible versions of the same library without conflict.
|
|
196
|
+
|
|
197
|
+
- An endpoint without a PEP 723 block runs in the base environment, with no extra cost.
|
|
198
|
+
- Installation happens on first startup and whenever the dependency block changes, not on every request.
|
|
199
|
+
|
|
200
|
+
> **Security note**: installing dependencies automatically after a `git pull` means running third-party code at deploy time. If you prefer to control that step, start with `--no-install` and Bitpoint will fail with a clear error on endpoints whose dependencies are not already installed.
|
|
201
|
+
|
|
202
|
+
## Shared code
|
|
203
|
+
|
|
204
|
+
The `routes/lib/` directory is reserved: it does not generate routes and is importable from any endpoint:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
# routes/lib/db.py
|
|
208
|
+
|
|
209
|
+
def get_connection():
|
|
210
|
+
...
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
# routes/users/get.py
|
|
215
|
+
|
|
216
|
+
from lib.db import get_connection
|
|
217
|
+
|
|
218
|
+
def handle(request):
|
|
219
|
+
conn = get_connection()
|
|
220
|
+
...
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Configuration
|
|
224
|
+
|
|
225
|
+
Everything is controlled from the command line, there is no configuration file:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
python -m bitpoint [options]
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Option | Default | Description |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `--port` | `8000` | Listening port |
|
|
234
|
+
| `--host` | `127.0.0.1` | Listening address |
|
|
235
|
+
| `--dir` | `./routes` | Root directory for endpoints |
|
|
236
|
+
| `--production` | disabled | Hides tracebacks and disables filesystem-based hot reload |
|
|
237
|
+
| `--no-install` | disabled | Does not install dependencies automatically |
|
|
238
|
+
|
|
239
|
+
## Hot reload
|
|
240
|
+
|
|
241
|
+
In development mode, Bitpoint watches the `routes` directory and reloads each module when its file changes. There is no shared state across reloads: if your endpoint keeps in-memory state (caches, connections), it is lost on reload. For persistent state use external resources (a database, Redis, files).
|
|
242
|
+
|
|
243
|
+
In production, filesystem-based reload is disabled. The recommended deploy flow is `git pull` followed by a reload signal:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
git pull && kill -HUP $(cat bitpoint.pid)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## A real example
|
|
250
|
+
|
|
251
|
+
A GitHub webhook that sends a Telegram message on every push:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
# routes/webhooks/github/post.py
|
|
255
|
+
|
|
256
|
+
# /// script
|
|
257
|
+
# dependencies = ["requests"]
|
|
258
|
+
# ///
|
|
259
|
+
import os
|
|
260
|
+
import requests
|
|
261
|
+
|
|
262
|
+
def handle(request):
|
|
263
|
+
if request.headers.get("x-github-event") != "push":
|
|
264
|
+
return None
|
|
265
|
+
payload = request.json()
|
|
266
|
+
pusher = payload["pusher"]["name"]
|
|
267
|
+
repo = payload["repository"]["name"]
|
|
268
|
+
commits = len(payload["commits"])
|
|
269
|
+
requests.post(
|
|
270
|
+
f"https://api.telegram.org/bot{os.environ['TELEGRAM_TOKEN']}/sendMessage",
|
|
271
|
+
json={
|
|
272
|
+
"chat_id": os.environ["TELEGRAM_CHAT_ID"],
|
|
273
|
+
"text": f"{pusher} pushed {commits} commit(s) to {repo}",
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
return None
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
One file, deployed with a `git pull`. No project, no virtualenv, no route registration, no restart.
|
|
280
|
+
|
|
281
|
+
## Why not Flask or FastAPI?
|
|
282
|
+
|
|
283
|
+
Use Flask or FastAPI when the API is your product: they are excellent frameworks and Bitpoint does not try to compete with them.
|
|
284
|
+
|
|
285
|
+
Bitpoint is for when the API is just plumbing. A webhook here, an internal endpoint there, a mock for the frontend. In those cases a framework gives you power you will not use at the price of ceremony you have to pay every time: a project to scaffold, an app to configure, routes to register, a virtualenv to maintain and a server to restart. Bitpoint removes all of that by turning the decisions into conventions: the filesystem is the router, the file is the endpoint, and its header declares its dependencies.
|
|
286
|
+
|
|
287
|
+
If an endpoint outgrows Bitpoint, nothing locks you in: `handle` is a plain function, and moving it to a Flask or FastAPI route is a copy-paste.
|
|
288
|
+
|
|
289
|
+
## Small core
|
|
290
|
+
|
|
291
|
+
Some areas are explicitly not covered in order to keep the core small:
|
|
292
|
+
|
|
293
|
+
- Middleware and global hooks
|
|
294
|
+
- Authentication
|
|
295
|
+
- WebSockets and streaming
|
|
296
|
+
- `async` endpoints
|
|
297
|
+
- Static files
|
|
298
|
+
|
|
299
|
+
If you need any of this today, Bitpoint is not your tool, you will have to use external tools.
|
bitpoint-0.1.0/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Bitpoint
|
|
2
|
+
|
|
3
|
+
Create HTTP endpoints quickly using files, without going through a framework like Flask or FastAPI.
|
|
4
|
+
|
|
5
|
+
- **File-based routing**: the directory structure defines the routes, the file name defines the HTTP method.
|
|
6
|
+
- **Hot reload**: save the file and the server updates automatically. Deploy with a simple `git pull`.
|
|
7
|
+
- **Per-endpoint dependencies**: each endpoint declares its dependencies with [PEP 723](https://peps.python.org/pep-0723/) and they are installed automatically in an isolated environment.
|
|
8
|
+
|
|
9
|
+
Bitpoint is for the moments when a full project feels like too much: exposing a script as a webhook, mocking an API so the frontend can move forward, publishing a small internal service for your team, or keeping a handful of endpoints alive on a personal server. In all these cases the API is not the product, it is just plumbing, and spending an afternoon on scaffolding, virtual environments and deploy pipelines is out of proportion. With Bitpoint you write one file and you get one endpoint.
|
|
10
|
+
|
|
11
|
+
Under the hood, Bitpoint stays out of your way. There is nothing to register and nothing to configure: no app object, no decorators, no config files, just a `handle` function per file. Dependency isolation is delegated to [uv](https://docs.astral.sh/uv/), with a cached environment per endpoint, so two endpoints can use incompatible versions of the same library without ever conflicting. Responses follow the type you return: a string becomes plain text, a dict becomes JSON, a tuple adds a status code and headers, and the sensible thing happens by default.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install bitpoint
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or, if you use [uv](https://docs.astral.sh/uv/):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install bitpoint
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Python 3.9 or later. If you want per-endpoint dependencies, `uv` must be available on the server.
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
Create a directory called `routes` and, inside it, a subdirectory called `hello` (this will be the route). Inside `hello`, create a file called `get.py` with the following content:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# routes/hello/get.py
|
|
33
|
+
|
|
34
|
+
def handle(request):
|
|
35
|
+
return "world"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Start the server with:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python -m bitpoint
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You can now try your endpoint:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl localhost:8000/hello
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
It will return:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
world
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Routing
|
|
57
|
+
|
|
58
|
+
The URL path maps to the directory path inside `routes`, and the HTTP method maps to the file name:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
routes/
|
|
62
|
+
├── hello/
|
|
63
|
+
│ └── get.py → GET /hello
|
|
64
|
+
├── users/
|
|
65
|
+
│ ├── get.py → GET /users
|
|
66
|
+
│ ├── post.py → POST /users
|
|
67
|
+
│ └── [id]/
|
|
68
|
+
│ ├── get.py → GET /users/42
|
|
69
|
+
│ └── delete.py → DELETE /users/42
|
|
70
|
+
└── lib/ → shared code, does not generate routes
|
|
71
|
+
└── db.py
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Valid file names are the HTTP methods in lowercase: `get.py`, `post.py`, `put.py`, `patch.py`, `delete.py`, `head.py` and `options.py`. Any other file is ignored and does not generate routes, so you can keep helper modules next to your endpoints.
|
|
75
|
+
|
|
76
|
+
### Dynamic routes
|
|
77
|
+
|
|
78
|
+
A directory whose name is wrapped in brackets captures a URL segment:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# routes/users/[id]/get.py
|
|
82
|
+
|
|
83
|
+
def handle(request):
|
|
84
|
+
return {"user_id": request.params["id"]}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
curl localhost:8000/users/42
|
|
89
|
+
# {"user_id": "42"}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Captured values always arrive as `str`. Type conversion is the endpoint's responsibility.
|
|
93
|
+
|
|
94
|
+
## The `handle` contract
|
|
95
|
+
|
|
96
|
+
Each endpoint exposes a `handle(request)` function. It is the only symbol Bitpoint looks for in the file, the rest of the module is yours.
|
|
97
|
+
|
|
98
|
+
### The `request` object
|
|
99
|
+
|
|
100
|
+
| Attribute | Type | Description |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `request.method` | `str` | HTTP method in uppercase, for example `"GET"` |
|
|
103
|
+
| `request.path` | `str` | Request path, for example `/users/42` |
|
|
104
|
+
| `request.params` | `dict[str, str]` | Dynamic path segments |
|
|
105
|
+
| `request.args` | `dict[str, str]` | Query string parameters |
|
|
106
|
+
| `request.headers` | `dict[str, str]` | Headers, case-insensitive keys |
|
|
107
|
+
| `request.body` | `bytes` | Raw request body |
|
|
108
|
+
| `request.json()` | `Any` | Body parsed as JSON, raises a 400 error if invalid |
|
|
109
|
+
|
|
110
|
+
Example with a query string:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# routes/greet/get.py
|
|
114
|
+
|
|
115
|
+
def handle(request):
|
|
116
|
+
name = request.args.get("name", "world")
|
|
117
|
+
return f"Hello, {name}!"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
curl "localhost:8000/greet?name=Bob"
|
|
122
|
+
# Hello, Bob!
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Return values
|
|
126
|
+
|
|
127
|
+
The return type determines the response:
|
|
128
|
+
|
|
129
|
+
| Return | Response |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `str` | `200`, `text/plain; charset=utf-8` |
|
|
132
|
+
| `dict` or `list` | `200`, `application/json` |
|
|
133
|
+
| `bytes` | `200`, `application/octet-stream` |
|
|
134
|
+
| `(body, status)` | As above, with the given status code |
|
|
135
|
+
| `(body, status, headers)` | Additionally with custom headers |
|
|
136
|
+
| `None` | `204 No Content` |
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# routes/users/post.py
|
|
140
|
+
|
|
141
|
+
def handle(request):
|
|
142
|
+
data = request.json()
|
|
143
|
+
return {"created": data["name"]}, 201
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Errors
|
|
147
|
+
|
|
148
|
+
- If `handle` raises an exception, Bitpoint responds with `500`.
|
|
149
|
+
- In development mode (the default) the body includes the traceback. In production (`--production`) the body is a generic message and the traceback goes to the log.
|
|
150
|
+
- Path with no matching directory: `404`. Directory exists but there is no file for that method: `405` with the `Allow` header listing the available methods.
|
|
151
|
+
|
|
152
|
+
## Dependencies
|
|
153
|
+
|
|
154
|
+
Declare each endpoint's dependencies with a [PEP 723](https://peps.python.org/pep-0723/) block at the top of the file, the same format understood by `uv` and `pipx`:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# routes/status/get.py
|
|
158
|
+
|
|
159
|
+
# /// script
|
|
160
|
+
# dependencies = ["requests"]
|
|
161
|
+
# ///
|
|
162
|
+
|
|
163
|
+
def handle(request):
|
|
164
|
+
import requests
|
|
165
|
+
return requests.get("https://example.com").json()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Bitpoint delegates resolution and installation to [uv](https://docs.astral.sh/uv/): each endpoint runs with its dependencies in an isolated, cached environment. Two endpoints can use incompatible versions of the same library without conflict.
|
|
169
|
+
|
|
170
|
+
- An endpoint without a PEP 723 block runs in the base environment, with no extra cost.
|
|
171
|
+
- Installation happens on first startup and whenever the dependency block changes, not on every request.
|
|
172
|
+
|
|
173
|
+
> **Security note**: installing dependencies automatically after a `git pull` means running third-party code at deploy time. If you prefer to control that step, start with `--no-install` and Bitpoint will fail with a clear error on endpoints whose dependencies are not already installed.
|
|
174
|
+
|
|
175
|
+
## Shared code
|
|
176
|
+
|
|
177
|
+
The `routes/lib/` directory is reserved: it does not generate routes and is importable from any endpoint:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# routes/lib/db.py
|
|
181
|
+
|
|
182
|
+
def get_connection():
|
|
183
|
+
...
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
# routes/users/get.py
|
|
188
|
+
|
|
189
|
+
from lib.db import get_connection
|
|
190
|
+
|
|
191
|
+
def handle(request):
|
|
192
|
+
conn = get_connection()
|
|
193
|
+
...
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Configuration
|
|
197
|
+
|
|
198
|
+
Everything is controlled from the command line, there is no configuration file:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
python -m bitpoint [options]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
| Option | Default | Description |
|
|
205
|
+
|---|---|---|
|
|
206
|
+
| `--port` | `8000` | Listening port |
|
|
207
|
+
| `--host` | `127.0.0.1` | Listening address |
|
|
208
|
+
| `--dir` | `./routes` | Root directory for endpoints |
|
|
209
|
+
| `--production` | disabled | Hides tracebacks and disables filesystem-based hot reload |
|
|
210
|
+
| `--no-install` | disabled | Does not install dependencies automatically |
|
|
211
|
+
|
|
212
|
+
## Hot reload
|
|
213
|
+
|
|
214
|
+
In development mode, Bitpoint watches the `routes` directory and reloads each module when its file changes. There is no shared state across reloads: if your endpoint keeps in-memory state (caches, connections), it is lost on reload. For persistent state use external resources (a database, Redis, files).
|
|
215
|
+
|
|
216
|
+
In production, filesystem-based reload is disabled. The recommended deploy flow is `git pull` followed by a reload signal:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
git pull && kill -HUP $(cat bitpoint.pid)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## A real example
|
|
223
|
+
|
|
224
|
+
A GitHub webhook that sends a Telegram message on every push:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# routes/webhooks/github/post.py
|
|
228
|
+
|
|
229
|
+
# /// script
|
|
230
|
+
# dependencies = ["requests"]
|
|
231
|
+
# ///
|
|
232
|
+
import os
|
|
233
|
+
import requests
|
|
234
|
+
|
|
235
|
+
def handle(request):
|
|
236
|
+
if request.headers.get("x-github-event") != "push":
|
|
237
|
+
return None
|
|
238
|
+
payload = request.json()
|
|
239
|
+
pusher = payload["pusher"]["name"]
|
|
240
|
+
repo = payload["repository"]["name"]
|
|
241
|
+
commits = len(payload["commits"])
|
|
242
|
+
requests.post(
|
|
243
|
+
f"https://api.telegram.org/bot{os.environ['TELEGRAM_TOKEN']}/sendMessage",
|
|
244
|
+
json={
|
|
245
|
+
"chat_id": os.environ["TELEGRAM_CHAT_ID"],
|
|
246
|
+
"text": f"{pusher} pushed {commits} commit(s) to {repo}",
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
return None
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
One file, deployed with a `git pull`. No project, no virtualenv, no route registration, no restart.
|
|
253
|
+
|
|
254
|
+
## Why not Flask or FastAPI?
|
|
255
|
+
|
|
256
|
+
Use Flask or FastAPI when the API is your product: they are excellent frameworks and Bitpoint does not try to compete with them.
|
|
257
|
+
|
|
258
|
+
Bitpoint is for when the API is just plumbing. A webhook here, an internal endpoint there, a mock for the frontend. In those cases a framework gives you power you will not use at the price of ceremony you have to pay every time: a project to scaffold, an app to configure, routes to register, a virtualenv to maintain and a server to restart. Bitpoint removes all of that by turning the decisions into conventions: the filesystem is the router, the file is the endpoint, and its header declares its dependencies.
|
|
259
|
+
|
|
260
|
+
If an endpoint outgrows Bitpoint, nothing locks you in: `handle` is a plain function, and moving it to a Flask or FastAPI route is a copy-paste.
|
|
261
|
+
|
|
262
|
+
## Small core
|
|
263
|
+
|
|
264
|
+
Some areas are explicitly not covered in order to keep the core small:
|
|
265
|
+
|
|
266
|
+
- Middleware and global hooks
|
|
267
|
+
- Authentication
|
|
268
|
+
- WebSockets and streaming
|
|
269
|
+
- `async` endpoints
|
|
270
|
+
- Static files
|
|
271
|
+
|
|
272
|
+
If you need any of this today, Bitpoint is not your tool, you will have to use external tools.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Bitpoint: create HTTP endpoints quickly using files.
|
|
2
|
+
|
|
3
|
+
This module is intentionally import-light: it is imported inside the
|
|
4
|
+
per-endpoint environments (which only need `bitpoint.request` and
|
|
5
|
+
`bitpoint.response`), so nothing here may import Flask.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|