diff --git a/.github/workflows/release-CI.yml b/.github/workflows/release-CI.yml index 7557b1f34..c16b1b77f 100644 --- a/.github/workflows/release-CI.yml +++ b/.github/workflows/release-CI.yml @@ -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 @@ -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/* diff --git a/Cargo.lock b/Cargo.lock index c5cf7985d..19941f851 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1516,7 +1516,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "robyn" -version = "0.68.0" +version = "0.69.0" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 4e6663ca1..a06e142d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "robyn" -version = "0.68.0" +version = "0.69.0" authors = ["Sanskar Jethi "] edition = "2021" description = "Robyn is a Super Fast Async Python Web Framework with a Rust runtime." diff --git a/pyproject.toml b/pyproject.toml index cb394e818..293f417b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } @@ -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 "] diff --git a/robyn/robyn.pyi b/robyn/robyn.pyi index ca0866ce1..abe9e5d93 100644 --- a/robyn/robyn.pyi +++ b/robyn/robyn.pyi @@ -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: """ diff --git a/src/types/headers.rs b/src/types/headers.rs index 27acf89c3..9fb2bf97c 100644 --- a/src/types/headers.rs +++ b/src/types/headers.rs @@ -97,6 +97,21 @@ impl Headers { dict.into() } + pub fn to_dict(&self, py: Python) -> Py { + // 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); diff --git a/unit_tests/test_request_object.py b/unit_tests/test_request_object.py index aa3f7a117..cef1a61c9 100644 --- a/unit_tests/test_request_object.py +++ b/unit_tests/test_request_object.py @@ -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"