diff --git a/.github/workflows/maturin.yml b/.github/workflows/maturin.yml index 691f0fb..b0947b2 100644 --- a/.github/workflows/maturin.yml +++ b/.github/workflows/maturin.yml @@ -1,7 +1,9 @@ -# This file is autogenerated by maturin v1.9.4 +# This file is autogenerated by maturin v1.9.5 # To update, run # -# maturin generate-ci github +# maturin generate-ci github --pytest -m python/Cargo.toml -o .github/workflows/pytest.yml +# +# NOTE: this file has been modified to add mypy and force installing the packages from the dist directory # name: Maturin @@ -45,7 +47,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} - args: --release --out dist --find-interpreter -m python/Cargo.toml + args: --release --out dist --find-interpreter --manifest-path python/Cargo.toml sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: auto - name: Upload wheels @@ -53,6 +55,31 @@ jobs: with: name: wheels-linux-${{ matrix.platform.target }} path: dist + - name: pytest + if: ${{ startsWith(matrix.platform.target, 'x86_64') }} + shell: bash + run: | + set -e + python3 -m venv .venv + source .venv/bin/activate + pip install pyfaup-rs --no-index --find-links dist --force-reinstall + pip install pytest mypy + cd python && pytest && mypy . + - name: pytest + if: ${{ !startsWith(matrix.platform.target, 'x86') && matrix.platform.target != 'ppc64' }} + uses: uraimo/run-on-arch-action@v2 + with: + arch: ${{ matrix.platform.target }} + distro: ubuntu22.04 + githubToken: ${{ github.token }} + install: | + apt-get update + apt-get install -y --no-install-recommends python3 python3-pip + pip3 install -U pip pytest mypy + run: | + set -e + pip3 install pyfaup-rs --no-index --find-links dist --force-reinstall + cd python && pytest && mypy . musllinux: runs-on: ${{ matrix.platform.runner }} @@ -76,7 +103,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} - args: --release --out dist --find-interpreter -m python/Cargo.toml + args: --release --out dist --find-interpreter --manifest-path python/Cargo.toml sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} manylinux: musllinux_1_2 - name: Upload wheels @@ -84,6 +111,36 @@ jobs: with: name: wheels-musllinux-${{ matrix.platform.target }} path: dist + - name: pytest + if: ${{ startsWith(matrix.platform.target, 'x86_64') }} + uses: addnab/docker-run-action@v3 + with: + image: alpine:latest + options: -v ${{ github.workspace }}:/io -w /io + run: | + set -e + apk add py3-pip py3-virtualenv + python3 -m virtualenv .venv + source .venv/bin/activate + pip install pyfaup-rs --no-index --find-links dist --force-reinstall + pip install pytest mypy + cd python && pytest && mypy . + - name: pytest + if: ${{ !startsWith(matrix.platform.target, 'x86') }} + uses: uraimo/run-on-arch-action@v2 + with: + arch: ${{ matrix.platform.target }} + distro: alpine_latest + githubToken: ${{ github.token }} + install: | + apk add py3-virtualenv + run: | + set -e + python3 -m virtualenv .venv + source .venv/bin/activate + pip install pytest mypy + pip install pyfaup-rs --no-index --find-links dist --force-reinstall + cd python && pytest && mypy . windows: runs-on: ${{ matrix.platform.runner }} @@ -104,13 +161,24 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} - args: --release --out dist --find-interpreter -m python/Cargo.toml + args: --release --out dist --find-interpreter --manifest-path python/Cargo.toml sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + maturin-version: 1.9.4 - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-windows-${{ matrix.platform.target }} path: dist + - name: pytest + if: ${{ !startsWith(matrix.platform.target, 'aarch64') }} + shell: bash + run: | + set -e + python3 -m venv .venv + source .venv/Scripts/activate + pip install pyfaup-rs --no-index --find-links dist --force-reinstall + pip install pytest mypy + cd python && pytest && mypy . macos: runs-on: ${{ matrix.platform.runner }} @@ -130,13 +198,21 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} - args: --release --out dist --find-interpreter -m python/Cargo.toml + args: --release --out dist --find-interpreter --manifest-path python/Cargo.toml sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - name: Upload wheels uses: actions/upload-artifact@v4 with: name: wheels-macos-${{ matrix.platform.target }} path: dist + - name: pytest + run: | + set -e + python3 -m venv .venv + source .venv/bin/activate + pip install pyfaup-rs --no-index --find-links dist --force-reinstall + pip install pytest mypy + cd python && pytest && mypy . sdist: runs-on: ubuntu-latest @@ -146,7 +222,7 @@ jobs: uses: PyO3/maturin-action@v1 with: command: sdist - args: --out dist -m python/Cargo.toml + args: --out dist --manifest-path python/Cargo.toml - name: Upload sdist uses: actions/upload-artifact@v4 with: diff --git a/python/py.typed b/python/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/python/pyfaup.pyi b/python/pyfaup.pyi new file mode 100644 index 0000000..3b36725 --- /dev/null +++ b/python/pyfaup.pyi @@ -0,0 +1,61 @@ +class FaupCompat: + + url: bytes + + def __init__(self, url: str | None=None) -> None: + ... + + def decode(self, url: str) -> None: + ... + + def get_credential(self) -> str | None: + ... + + def get_domain(self) -> str | None: + ... + + def get_subdomain(self) -> str | None: + ... + + def get_fragment(self) -> str | None: + ... + + def get_host(self) -> str | None: + ... + + def get_resource_path(self) -> str | None: + ... + + def get_tld(self) -> str | None: + ... + + def get_query_string(self) -> str | None: + ... + + def get_scheme(self) -> str | None: + ... + + def get_domain_without_tld(self) -> str | None: + ... + + def get_port(self) -> int | None: + ... + + +class Url: + + orig: str + scheme: str + username: str | None + password: str | None + host: str + subdomain: str | None + domain: str | None + suffix: str | None + port: int | None + path: str | None + query: str | None + fragment: str | None + + def __init__(self, url: str | None = None) -> None: + ... diff --git a/python/pyproject.toml b/python/pyproject.toml index 4a8f6f2..818ae0f 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=1.9,<2.0"] +requires = ["maturin (>=1.9.4,!=1.9.5,<2.0)"] build-backend = "maturin" [project] diff --git a/python/src/lib.rs b/python/src/lib.rs index 1d374e9..9d065b9 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -42,6 +42,8 @@ impl From for PyErr { /// >>> print(url.port) # 8080 #[pyclass] pub struct Url { + #[pyo3(get)] + pub orig: String, #[pyo3(get)] pub scheme: String, #[pyo3(get)] @@ -91,6 +93,7 @@ impl From> for Url { }; Self { + orig: value.as_str().into(), scheme: value.scheme().into(), username, password, @@ -140,6 +143,7 @@ impl Url { .map(|u| u.into()) .map_err(|e| PyValueError::new_err(e.to_string())) } + } /// A compatibility class that mimics the FAUP (Fast URL Parser) Python API. @@ -233,6 +237,59 @@ impl FaupCompat { Ok(m) } + + fn get_credential(&self) -> Option { + let url = self.url.as_ref(); + url.and_then(|u| u.credentials()) + } + + fn get_domain(&self) -> Option<&str> { + self.url.as_ref()?.domain.as_deref() + } + + fn get_subdomain(&self) -> Option<&str> { + self.url.as_ref()?.subdomain.as_deref() + } + + fn get_fragment(&self) -> Option<&str> { + self.url.as_ref()?.fragment.as_deref() + } + + fn get_host(&self) -> Option<&str> { + self.url.as_ref().map(|u| u.host.as_str()) + } + + fn get_resource_path(&self) -> Option<&str> { + self.url.as_ref()?.path.as_deref() + } + + fn get_tld(&self) -> Option<&str> { + self.url.as_ref()?.suffix.as_deref() + } + + fn get_query_string(&self) -> Option<&str> { + self.url.as_ref()?.query.as_deref() + } + + fn get_scheme(&self) -> Option<&str> { + self.url.as_ref().map(|u| u.scheme.as_str()) + } + + fn get_port(&self) -> Option { + self.url.as_ref()?.port + } + + fn get_domain_without_tld(&self) -> Option<&str> { + if let (Some(domain), Some(tld)) = (self.get_domain(), self.get_tld()) { + domain + .strip_suffix(tld) + .and_then(|dom| dom.strip_suffix('.')) + } + else { + None + } + } + } /// A Python module implemented in Rust for URL parsing. diff --git a/python/tests/test_pyfaup.py b/python/tests/test_pyfaup.py new file mode 100644 index 0000000..71518e3 --- /dev/null +++ b/python/tests/test_pyfaup.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import unittest + +from pyfaup import Url + + +class TestPyFaupRR(unittest.TestCase): + + def test_url(self) -> None: + parsed_url = Url('https://user:pass@sub.example.com:8080/path?query=value#fragment') + + self.assertEqual(parsed_url.orig, 'https://user:pass@sub.example.com:8080/path?query=value#fragment') + + self.assertEqual(parsed_url.scheme, 'https') + self.assertEqual(parsed_url.username, 'user') + self.assertEqual(parsed_url.password, 'pass') + self.assertEqual(parsed_url.host, 'sub.example.com') + self.assertEqual(parsed_url.subdomain, 'sub') + self.assertEqual(parsed_url.domain, 'example.com') + self.assertEqual(parsed_url.suffix, 'com') + self.assertEqual(parsed_url.port, 8080) + self.assertEqual(parsed_url.path, '/path') + self.assertEqual(parsed_url.query, 'query=value') + self.assertEqual(parsed_url.fragment, 'fragment')