Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/release-CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ jobs:
runs-on: ubuntu-latest
needs: [macos, windows, linux, linux-cross, merge]
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v4
with:
name: wheels
Expand All @@ -189,10 +190,16 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: 3.x
- uses: dtolnay/rust-toolchain@stable
- name: Build source distribution
uses: PyO3/maturin-action@v1
with:
command: sdist
args: -i python --out dist
- name: Publish to PyPi
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
uv pip install --upgrade twine
twine upload --skip-existing *
twine upload --skip-existing * dist/*
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "robyn"
version = "0.68.0"
version = "0.69.0"
authors = ["Sanskar Jethi <sansyrox@gmail.com>"]
edition = "2021"
description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime."
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "robyn"
version = "0.68.0"
version = "0.69.0"
description = "A Super Fast Async Python Web Framework with a Rust runtime."
authors = [{ name = "Sanskar Jethi", email = "sansyrox@gmail.com" }]
license = { file = "LICENSE" }
Expand Down Expand Up @@ -66,7 +66,7 @@ test = [

[tool.poetry]
name = "robyn"
version = "0.68.0"
version = "0.69.0"
description = "A Super Fast Async Python Web Framework with a Rust runtime."
authors = ["Sanskar Jethi <sansyrox@gmail.com>"]

Expand Down
19 changes: 19 additions & 0 deletions robyn/robyn.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,25 @@ class Headers:
"""
pass

def get_headers(self) -> dict[str, list[str]]:
"""
Returns all headers as a dictionary where keys are header names and values are lists of all values for that header.

Returns:
dict[str, list[str]]: Dictionary mapping header names to lists of values
"""
pass

def to_dict(self) -> dict[str, str]:
"""
Returns headers as a flattened dictionary, joining duplicate headers with commas (Flask-style).
This allows using dict.get() with default values for headers.

Returns:
dict[str, str]: Dictionary mapping header names to comma-separated values
"""
pass

@dataclass
class Request:
"""
Expand Down
15 changes: 15 additions & 0 deletions src/types/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@ impl Headers {
dict.into()
}

pub fn to_dict(&self, py: Python) -> Py<PyDict> {
// return as a flattened dict, joining duplicate headers with commas (Flask-style)
let dict = PyDict::new(py);
for iter in self.headers.iter() {
let (key, values) = iter.pair();
let joined_value = if values.len() == 1 {
values[0].clone()
} else {
values.join(",")
};
dict.set_item(key, joined_value).unwrap();
}
dict.into()
}

pub fn contains(&self, key: String) -> bool {
debug!("Checking if header {} exists", key);
debug!("Headers: {:?}", self.headers);
Expand Down
56 changes: 56 additions & 0 deletions unit_tests/test_request_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,59 @@ def test_request_object():
print(request.headers.get("Content-Type"))
assert request.headers.get("Content-Type") == "application/json"
assert request.method == "GET"


def test_headers_to_dict():
# Test single header values
headers = Headers({"Content-Type": "application/json", "Authorization": "Bearer token"})
headers_dict = headers.to_dict()

assert headers_dict["content-type"] == "application/json"
assert headers_dict["authorization"] == "Bearer token"

# Test with default values (Flask-style behavior)
custom_header = headers_dict.get("x-custom", "default")
assert custom_header == "default"

user_agent = headers_dict.get("user-agent", "blank/None")
assert user_agent == "blank/None"


def test_headers_to_dict_with_duplicates():
# Test duplicate header values (joined with commas)
headers = Headers({})
headers.append("X-Custom", "value1")
headers.append("X-Custom", "value2")
headers.append("X-Custom", "value3")

headers_dict = headers.to_dict()

# Should join multiple values with commas (Flask-style)
assert headers_dict["x-custom"] == "value1,value2,value3"


def test_headers_to_dict_vs_get_headers():
# Compare to_dict() with get_headers() behavior
headers = Headers({})
headers.set("Content-Type", "application/json")
headers.append("X-Custom", "value1")
headers.append("X-Custom", "value2")

# get_headers returns dict of lists
headers_lists = headers.get_headers()
assert headers_lists["content-type"] == ["application/json"]
assert headers_lists["x-custom"] == ["value1", "value2"]

# to_dict returns flattened dict with comma-separated values
headers_dict = headers.to_dict()
assert headers_dict["content-type"] == "application/json"
assert headers_dict["x-custom"] == "value1,value2"


def test_headers_to_dict_empty():
# Test empty headers
headers = Headers({})
headers_dict = headers.to_dict()

assert headers_dict == {}
assert headers_dict.get("any-header", "default") == "default"
Loading