Skip to content

Commit cccc41e

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 5d763fa + 0cdd937 commit cccc41e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+9042
-1599
lines changed

.env.example

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
DEFAULT_SERVER_PORT=8080
2+
DEFAULT_SERVER_HOST=localhost
3+
APP_ENTRYPOINT=lightbug.🔥

.github/workflows/branch.yml

+12
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,15 @@ jobs:
2020

2121
package:
2222
uses: ./.github/workflows/package.yml
23+
24+
docker:
25+
needs: package
26+
uses: ./.github/workflows/docker-build.yml
27+
with:
28+
tags: |
29+
type=ref,event=branch
30+
type=sha,format=long
31+
secrets:
32+
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
33+
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
34+

.github/workflows/docker-build.yml

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Docker Build and Push
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
tags:
7+
required: true
8+
type: string
9+
secrets:
10+
DOCKERHUB_USERNAME:
11+
required: true
12+
DOCKERHUB_TOKEN:
13+
required: true
14+
15+
jobs:
16+
docker:
17+
name: Build and push Docker image
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v4
22+
23+
- name: Docker meta
24+
id: meta
25+
uses: docker/metadata-action@v5
26+
with:
27+
images: ${{ secrets.DOCKERHUB_USERNAME }}/lightbug_http
28+
tags: ${{ inputs.tags }}
29+
30+
- name: Set up Docker Buildx
31+
uses: docker/setup-buildx-action@v3
32+
33+
- name: Login to Docker Hub
34+
uses: docker/login-action@v3
35+
with:
36+
username: ${{ secrets.DOCKERHUB_USERNAME }}
37+
password: ${{ secrets.DOCKERHUB_TOKEN }}
38+
39+
- name: Build and push
40+
uses: docker/build-push-action@v5
41+
with:
42+
context: .
43+
file: ./docker/lightbug.dockerfile
44+
push: true
45+
tags: ${{ steps.meta.outputs.tags }}
46+
labels: ${{ steps.meta.outputs.labels }}
47+
cache-from: type=gha
48+
cache-to: type=gha,mode=max

.github/workflows/main.yml

+11-1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,14 @@ jobs:
1919
secrets:
2020
PREFIX_API_KEY: ${{ secrets.PREFIX_API_KEY }}
2121

22-
22+
docker:
23+
needs: [package, publish]
24+
uses: ./.github/workflows/docker-build.yml
25+
with:
26+
tags: |
27+
type=raw,value=edge
28+
type=raw,value=stable
29+
type=sha,prefix=stable-
30+
secrets:
31+
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
32+
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}

.github/workflows/release.yml

+12
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,15 @@ jobs:
4040
file: lightbug_http.mojopkg
4141
tag: ${{ steps.get_version.outputs.VERSION }}
4242
overwrite: true
43+
44+
docker:
45+
needs: [package, upload-to-release]
46+
uses: ./.github/workflows/docker-build.yml
47+
with:
48+
tags: |
49+
type=semver,pattern={{version}}
50+
type=semver,pattern={{major}}.{{minor}}
51+
type=raw,value=latest
52+
secrets:
53+
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
54+
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}

README.md

+67-43
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,16 @@ Lightbug is a simple and sweet HTTP framework for Mojo that builds on best pract
2929
This is not production ready yet. We're aiming to keep up with new developments in Mojo, but it might take some time to get to a point when this is safe to use in real-world applications.
3030

3131
Lightbug currently has the following features:
32-
- [x] Pure Mojo networking! No dependencies on Python by default
33-
- [x] TCP-based server and client implementation
34-
- [x] Assign your own custom handler to a route
35-
- [x] Craft HTTP requests and responses with built-in primitives
36-
- [x] Everything is fully typed, with no `def` functions used
32+
- [x] Pure Mojo! No Python dependencies. Everything is fully typed, with no `def` functions used
33+
- [x] HTTP Server and Client implementations
34+
- [x] TCP and UDP support
35+
- [x] Cookie support
3736

3837
### Check Out These Mojo Libraries:
3938

4039
- Logging - [@toasty/stump](https://github.com/thatstoasty/stump)
4140
- CLI and Terminal - [@toasty/prism](https://github.com/thatstoasty/prism), [@toasty/mog](https://github.com/thatstoasty/mog)
42-
- Date/Time - [@mojoto/morrow](https://github.com/mojoto/morrow.mojo) and [@toasty/small-time](https://github.com/thatstoasty/small-time)
41+
- Date/Time - [@mojoto/morrow](https://github.com/mojoto/morrow.mojo) and [@toasty/small_time](https://github.com/thatstoasty/small_time)
4342

4443
<p align="right">(<a href="#readme-top">back to top</a>)</p>
4544

@@ -61,7 +60,7 @@ Once you have a Mojo project set up locally,
6160

6261
```toml
6362
[dependencies]
64-
lightbug_http = ">=0.1.9"
63+
lightbug_http = ">=0.1.11"
6564
```
6665

6766
3. Run `magic install` at the root of your project, where `mojoproject.toml` is located
@@ -97,25 +96,22 @@ Once you have a Mojo project set up locally,
9796
For example, to make a `Printer` service that prints some details about the request to console:
9897

9998
```mojo
100-
from lightbug_http import *
99+
from lightbug_http.http import HTTPRequest, HTTPResponse, OK
100+
from lightbug_http.strings import to_string
101+
from lightbug_http.header import HeaderKey
101102
102103
@value
103104
struct Printer(HTTPService):
104105
fn func(mut self, req: HTTPRequest) raises -> HTTPResponse:
105-
var uri = req.uri
106-
print("Request URI: ", to_string(uri.request_uri))
107-
108-
var header = req.headers
109-
print("Request protocol: ", req.protocol)
110-
print("Request method: ", req.method)
111-
print(
112-
"Request Content-Type: ", to_string(header[HeaderKey.CONTENT_TYPE])
113-
)
114-
115-
var body = req.body_raw
116-
print("Request Body: ", to_string(body))
117-
118-
return OK(body)
106+
print("Request URI:", req.uri.request_uri)
107+
print("Request protocol:", req.protocol)
108+
print("Request method:", req.method)
109+
if HeaderKey.CONTENT_TYPE in req.headers:
110+
print("Request Content-Type:", req.headers[HeaderKey.CONTENT_TYPE])
111+
if req.body_raw:
112+
print("Request Body:", to_string(req.body_raw))
113+
114+
return OK(req.body_raw)
119115
```
120116

121117
6. Start a server listening on a port with your service like so.
@@ -126,12 +122,12 @@ Once you have a Mojo project set up locally,
126122
fn main() raises:
127123
var server = Server()
128124
var handler = Welcome()
129-
server.listen_and_serve("0.0.0.0:8080", handler)
125+
server.listen_and_serve("localhost:8080", handler)
130126
```
131127
132128
Feel free to change the settings in `listen_and_serve()` to serve on a particular host and port.
133129
134-
Now send a request `0.0.0.0:8080`. You should see some details about the request printed out to the console.
130+
Now send a request `localhost:8080`. You should see some details about the request printed out to the console.
135131
136132
Congrats 🥳 You're using Lightbug!
137133
@@ -200,19 +196,15 @@ from lightbug_http import *
200196
from lightbug_http.client import Client
201197
202198
fn test_request(mut client: Client) raises -> None:
203-
var uri = URI.parse_raises("http://httpbin.org/status/404")
204-
var headers = Header("Host", "httpbin.org")
205-
199+
var uri = URI.parse("google.com")
200+
var headers = Headers(Header("Host", "google.com"))
206201
var request = HTTPRequest(uri, headers)
207202
var response = client.do(request^)
208203
209204
# print status code
210205
print("Response:", response.status_code)
211206
212-
# print parsed headers (only some are parsed for now)
213-
print("Content-Type:", response.headers["Content-Type"])
214-
print("Content-Length", response.headers["Content-Length"])
215-
print("Server:", to_string(response.headers["Server"]))
207+
print(response.headers)
216208
217209
print(
218210
"Is connection set to connection-close? ", response.connection_close()
@@ -232,16 +224,50 @@ fn main() -> None:
232224

233225
Pure Mojo-based client is available by default. This client is also used internally for testing the server.
234226

235-
## Switching between pure Mojo and Python implementations
236-
237-
By default, Lightbug uses the pure Mojo implementation for networking. To use Python's `socket` library instead, just import the `PythonServer` instead of the `Server` with the following line:
227+
### UDP Support
228+
To get started with UDP, just use the `listen_udp` and `dial_udp` functions, along with `write_to` and `read_from` methods, like below.
238229

230+
On the client:
239231
```mojo
240-
from lightbug_http.python.server import PythonServer
232+
from lightbug_http.connection import dial_udp
233+
from lightbug_http.address import UDPAddr
234+
from utils import StringSlice
235+
236+
alias test_string = "Hello, lightbug!"
237+
238+
fn main() raises:
239+
print("Dialing UDP server...")
240+
alias host = "127.0.0.1"
241+
alias port = 12000
242+
var udp = dial_udp(host, port)
243+
244+
print("Sending " + str(len(test_string)) + " messages to the server...")
245+
for i in range(len(test_string)):
246+
_ = udp.write_to(str(test_string[i]).as_bytes(), host, port)
247+
248+
try:
249+
response, _, _ = udp.read_from(16)
250+
print("Response received:", StringSlice(unsafe_from_utf8=response))
251+
except e:
252+
if str(e) != str("EOF"):
253+
raise e
254+
241255
```
242256

243-
You can then use all the regular server commands in the same way as with the default server.
244-
Note: as of September, 2024, `PythonServer` and `PythonClient` throw a compilation error when starting. There's an open [issue](https://github.com/saviorand/lightbug_http/issues/41) to fix this - contributions welcome!
257+
On the server:
258+
```mojo
259+
fn main() raises:
260+
var listener = listen_udp("127.0.0.1", 12000)
261+
262+
while True:
263+
response, host, port = listener.read_from(16)
264+
var message = StringSlice(unsafe_from_utf8=response)
265+
print("Message received:", message)
266+
267+
# Response with the same message in uppercase
268+
_ = listener.write_to(String.upper(message).as_bytes(), host, port)
269+
270+
```
245271

246272
<!-- ROADMAP -->
247273
## Roadmap
@@ -252,19 +278,17 @@ Note: as of September, 2024, `PythonServer` and `PythonClient` throw a compilati
252278

253279
We're working on support for the following (contributors welcome!):
254280

255-
- [ ] [WebSocket Support](https://github.com/saviorand/lightbug_http/pull/57)
281+
- [ ] [JSON support](https://github.com/saviorand/lightbug_http/issues/4)
282+
- [ ] Complete HTTP/1.x support compliant with RFC 9110/9112 specs (see issues)
256283
- [ ] [SSL/HTTPS support](https://github.com/saviorand/lightbug_http/issues/20)
257-
- [ ] UDP support
258-
- [ ] [Better error handling](https://github.com/saviorand/lightbug_http/issues/3), [improved form/multipart and JSON support](https://github.com/saviorand/lightbug_http/issues/4)
259284
- [ ] [Multiple simultaneous connections](https://github.com/saviorand/lightbug_http/issues/5), [parallelization and performance optimizations](https://github.com/saviorand/lightbug_http/issues/6)
260285
- [ ] [HTTP 2.0/3.0 support](https://github.com/saviorand/lightbug_http/issues/8)
261-
- [ ] [ASGI spec conformance](https://github.com/saviorand/lightbug_http/issues/17)
262286

263287
The plan is to get to a feature set similar to Python frameworks like [Starlette](https://github.com/encode/starlette), but with better performance.
264288

265289
Our vision is to develop three libraries, with `lightbug_http` (this repo) as a starting point:
266-
- `lightbug_http` - HTTP infrastructure and basic API development
267-
- `lightbug_api` - (coming later in 2024!) Tools to make great APIs fast, with support for OpenAPI spec and domain driven design
290+
- `lightbug_http` - Lightweight and simple HTTP framework, basic networking primitives
291+
- [`lightbug_api`](https://github.com/saviorand/lightbug_api) - Tools to make great APIs fast, with OpenAPI support and automated docs
268292
- `lightbug_web` - (release date TBD) Full-stack web framework for Mojo, similar to NextJS or SvelteKit
269293

270294
The idea is to get to a point where the entire codebase of a simple modern web application can be written in Mojo.

benchmark/bench.mojo

+17-28
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ from memory import Span
22
from benchmark import *
33
from lightbug_http.io.bytes import bytes, Bytes
44
from lightbug_http.header import Headers, Header
5-
from lightbug_http.utils import ByteReader, ByteWriter
5+
from lightbug_http.io.bytes import ByteReader, ByteWriter
66
from lightbug_http.http import HTTPRequest, HTTPResponse, encode
77
from lightbug_http.uri import URI
88

@@ -11,9 +11,7 @@ alias headers = "GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mo
1111
alias body = "I am the body of an HTTP request" * 5
1212
alias body_bytes = bytes(body)
1313
alias Request = "GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n" + body
14-
alias Response = "HTTP/1.1 200 OK\r\nserver: lightbug_http\r\ncontent-type:"
15-
" application/octet-stream\r\nconnection: keep-alive\r\ncontent-length:"
16-
" 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\n\r\n" + body
14+
alias Response = "HTTP/1.1 200 OK\r\nserver: lightbug_http\r\ncontent-type: application/octet-stream\r\nconnection: keep-alive\r\ncontent-length: 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\n\r\n" + body
1715

1816

1917
fn main():
@@ -26,24 +24,12 @@ fn run_benchmark():
2624
config.verbose_timing = True
2725
config.tabular_view = True
2826
var m = Bench(config)
29-
m.bench_function[lightbug_benchmark_header_encode](
30-
BenchId("HeaderEncode")
31-
)
32-
m.bench_function[lightbug_benchmark_header_parse](
33-
BenchId("HeaderParse")
34-
)
35-
m.bench_function[lightbug_benchmark_request_encode](
36-
BenchId("RequestEncode")
37-
)
38-
m.bench_function[lightbug_benchmark_request_parse](
39-
BenchId("RequestParse")
40-
)
41-
m.bench_function[lightbug_benchmark_response_encode](
42-
BenchId("ResponseEncode")
43-
)
44-
m.bench_function[lightbug_benchmark_response_parse](
45-
BenchId("ResponseParse")
46-
)
27+
m.bench_function[lightbug_benchmark_header_encode](BenchId("HeaderEncode"))
28+
m.bench_function[lightbug_benchmark_header_parse](BenchId("HeaderParse"))
29+
m.bench_function[lightbug_benchmark_request_encode](BenchId("RequestEncode"))
30+
m.bench_function[lightbug_benchmark_request_parse](BenchId("RequestParse"))
31+
m.bench_function[lightbug_benchmark_response_encode](BenchId("ResponseEncode"))
32+
m.bench_function[lightbug_benchmark_response_parse](BenchId("ResponseParse"))
4733
m.dump_report()
4834
except:
4935
print("failed to start benchmark")
@@ -99,15 +85,19 @@ fn lightbug_benchmark_request_parse(mut b: Bencher):
9985
fn lightbug_benchmark_request_encode(mut b: Bencher):
10086
@always_inline
10187
@parameter
102-
fn request_encode():
88+
fn request_encode() raises:
89+
var uri = URI.parse("http://127.0.0.1:8080/some-path")
10390
var req = HTTPRequest(
104-
URI.parse("http://127.0.0.1:8080/some-path"),
91+
uri=uri,
10592
headers=headers_struct,
10693
body=body_bytes,
10794
)
10895
_ = encode(req^)
10996

110-
b.iter[request_encode]()
97+
try:
98+
b.iter[request_encode]()
99+
except e:
100+
print("failed to encode request, error: ", e)
111101

112102

113103
@parameter
@@ -130,8 +120,7 @@ fn lightbug_benchmark_header_parse(mut b: Bencher):
130120
var header = Headers()
131121
var reader = ByteReader(headers.as_bytes())
132122
_ = header.parse_raw(reader)
133-
except:
134-
print("failed")
123+
except e:
124+
print("failed", e)
135125

136126
b.iter[header_parse]()
137-

benchmark/bench_server.mojo

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def main():
66
try:
77
var server = Server(tcp_keep_alive=True)
88
var handler = TechEmpowerRouter()
9-
server.listen_and_serve("0.0.0.0:8080", handler)
9+
server.listen_and_serve("localhost:8080", handler)
1010
except e:
1111
print("Error starting server: " + e.__str__())
1212
return

0 commit comments

Comments
 (0)