Skip to content
Merged
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
92 changes: 83 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,103 @@ name: Release

permissions:
contents: write
id-token: write

on:
push:
tags:
- v[0-9]+.*
- "v[0-9]+.[0-9]+.[0-9]+"
workflow_dispatch:
Comment on lines 8 to +11
Comment on lines 8 to +11
inputs:
publish:
description: "Publish artifacts to PyPI (manual runs default to build-only)"
required: true
type: boolean
default: false

env:
CARGO_TERM_COLOR: always

jobs:
# make sure release content has correct format and README is up-to-date
format-check:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run format check
run: cargo fmt --check
- name: Run clippy
run: cargo clippy -- -D warnings

build-sdist:
needs: checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
- uses: actions/upload-artifact@v4
with:
name: sdist
path: dist/*.tar.gz

build-wheels:
needs: checks
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64
- os: ubuntu-latest
target: aarch64
- os: macos-13
target: x86_64
- os: macos-14
target: aarch64
- os: windows-latest
target: x64
steps:
- uses: actions/checkout@v4
- uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist
- uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}-${{ matrix.target }}
path: dist/*.whl

publish-pypi:
needs: [build-sdist, build-wheels]
runs-on: ubuntu-latest
if: github.event_name == 'push' || inputs.publish == true
environment: pypi
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
skip-existing: true

create-release:
needs: format-check
needs: [build-sdist, build-wheels, publish-pypi]
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: taiki-e/create-gh-release-action@v1
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
# (optional) Path to changelog.
changelog: CHANGELOG.md
# (required) GitHub token for creating GitHub Releases.
token: ${{ secrets.GITHUB_TOKEN }}
files: dist/*
body_path: CHANGELOG.md
24 changes: 18 additions & 6 deletions .github/workflows/rust.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,27 @@ env:
CARGO_TERM_COLOR: always

jobs:
build:

rust-checks:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- uses: actions/checkout@v4
- name: Run format check
run: cargo fmt --check

- name: Run clippy
run: cargo clippy -- -D warnings

python-api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install test tools
run: python -m pip install --upgrade pip maturin pytest pytest-benchmark
- name: Build wheel
run: maturin build --release --out dist
- name: Install built wheel
run: python -m pip install --force-reinstall dist/*.whl
- name: Run Python API tests (unit)
run: pytest tests/test_api.py -v
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ cache

.DS_Store

.pypirc
.pypirc
__pycache__/
*.py[cod]
83 changes: 83 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# PROJECT KNOWLEDGE BASE

## OVERVIEW

Python binding for `bgpkit-parser` (Rust MRT/BGP parser). Exposes `Parser` (full elems), `RouteParser` (route-level scans), `Filter` helpers, and projected tuple iteration via PyO3, built with maturin.

## STRUCTURE

```
├── src/lib.rs # Entire Python extension: Parser/elem/route PyO3 classes
├── examples/ # Python usage examples
├── Cargo.toml # Rust crate: pybgpkit-parser, depends on bgpkit-parser
├── pyproject.toml # Maturin build-system config
├── build.rs # PyO3 extension module linker setup
├── benches/ # Rust criterion benchmarks
├── tests/ # Python API tests and benchmark
└── .github/workflows/ # Rust fmt/clippy CI + tag-based release
```

## WHERE TO LOOK

| Task | Location |
|------|----------|
| Change exposed Python API | `src/lib.rs` |
| Update underlying parser logic | `Cargo.toml` → bump `bgpkit-parser` version |
| Build/test locally | `maturin develop` (see README.md) |
| Build wheels for release | GitHub Actions `release.yml` (push `v*` tag) |
| Publish to PyPI | Push `v*` tag; CI publishes via PyPI Trusted Publishing (OIDC) |

## CODE MAP

- **`Elem`** — PyO3 class wrapping a parsed BGP element. Has `#[pyo3(get, set)]` fields and `to_dict()` / `__str__` / `__getstate__` methods.
- **`Parser`** — PyO3 class wrapping `bgpkit_parser::BgpkitParser`. Constructor takes `url`, optional `filters` (HashMap), and optional `cache_dir`. Implements `__iter__`/`__next__`, `count`, `iter_batches`, `iter_tuples`, and `iter_tuple_batches`.
- **`RouteParser`** — PyO3 class wrapping `BgpkitParser::into_route_iter()`. Returns lightweight `RouteElem` values. Same iteration/helper surface as `Parser`.
- **`Filter`** — PyO3 class wrapping `bgpkit_parser::parser::Filter`. Constructors: `__init__`, `peer_ip`, `peer_ips`, `origin_asn`, `prefix`, `elem_type`.
- **`TupleIterator` / `TupleBatchIterator`** — High-performance projected tuple iteration for `Parser` and `RouteParser`.
- **`convert_elem`** — Internal fn mapping `BgpElem` → `Elem` (Rust type → PyO3 type).

## CONVENTIONS

- Rust fmt/clippy enforced in CI (`cargo fmt --check`, `cargo clippy -- -D warnings`)
- `PyValueError` used for filter errors propagated to Python
- Iterator-backed pyclasses use `#[pyclass(unsendable)]`; no `unsafe impl Send/Sync`
- `#[pyo3(name = "__str__")]` used for JSON string representation of `Elem`
- `atomic` field returns `"AG"`/`"NAG"` strings (not bool)
- `elem_type` field returns `"A"` (announce) or `"W"` (withdraw)

## ANTI-PATTERNS

- **Do NOT** change PyO3/maturin versions without updating both `Cargo.toml` and `build.rs` (`pyo3-build-config` must match)
- **Do NOT** test release publishing with a beta tag unless the package version is also beta; use `workflow_dispatch` with `publish=false` for build-only checks
- **Do NOT** add long-lived PyPI API tokens; use PyPI Trusted Publishing with GitHub OIDC (`environment: pypi`)
- **Do NOT** add `unsafe impl Send/Sync` to `#[pyclass]` types; use `#[pyclass(unsendable)]` instead
- **Do NOT** use `.unwrap()` on user inputs (URL/filters); already handled in `BgpkitParser::new` but be careful with new additions
- **Do NOT** make `Elem` fields write-only or remove getters without noting in CHANGELOG as breaking (v0.6.0 was a breaking change)

## COMMANDS

```bash
# Local dev build (installs to active venv)
maturin develop

# Build wheel locally
maturin build --release

# Build and publish release via CI
git tag v0.7.0
git push origin v0.7.0

# Format + lint
cargo fmt --check
cargo clippy -- -D warnings

# Publish (after building on all platforms)
twine upload --skip-existing target/wheels/*
```

## NOTES

- `bgpkit-parser` crate version bump is the primary release trigger (see CHANGELOG for version history)
- Release workflow: `rust.yaml` runs Rust + Python API checks on PR/push; `release.yml` builds ABI3 wheels and publishes on `v*` tag push via Trusted Publishing
- Supports Python 3.9+ via ABI3 wheels
- Python API tests live in `tests/test_api.py`; network smoke coverage is gated by `PYBGPKIT_RUN_NETWORK_TESTS=1`
72 changes: 54 additions & 18 deletions BUILD.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,67 @@
# Build and Publish Guide

## Pre-requisites
## Automated Release (Recommended)

- `maturin`
- `docker`
- run `docker build . -t bgpkit-builder:latest` to build the builder image
Release builds are handled by GitHub Actions via `.github/workflows/release.yml`.

## Build and Upload Checklist
Push a version tag to build and publish:

1. run [`build.sh`](./build.sh) on Apple Silicon Mac
2. run [`build.sh`](./build.sh) inside docker on Apple Silicon Mac
3. run [`build.sh`](./build.sh) on Intel Mac
4. run [`build.sh`](./build.sh) inside docker on Intel Mac

Then run
```
twine upload --skip-existing target/wheels/*
```bash
git tag v0.7.0
git push origin v0.7.0
```

## Build Linux packages in Docker
The release workflow will:

1. Run `cargo fmt --check` and `cargo clippy -- -D warnings`
2. Build the source distribution (`sdist`)
3. Build ABI3 wheels for:
- Linux x86_64
- Linux aarch64
- macOS x86_64
- macOS arm64
- Windows x86_64
4. Publish artifacts to PyPI using PyPI Trusted Publishing (OIDC)
5. Create a GitHub Release and attach the built artifacts

Manual workflow runs (`workflow_dispatch`) are build-only by default. They only publish when the `publish` input is explicitly enabled.

## PyPI Trusted Publishing Setup

Configure a trusted publisher for the existing `pybgpkit-parser` PyPI project:

| Field | Value |
|------|-------|
| Owner | `bgpkit` |
| Repository name | `bgpkit-parser-py` |
| Workflow name | `release.yml` |
| Environment name | `pypi` |

The workflow uses GitHub Actions OIDC (`id-token: write`) and does not require a long-lived PyPI API token.

## Local Development Build

Build image using the [Dockerfile](./Dockerfile) provided
```bash
maturin develop
```
docker build -t bgpkit-builder:latest .

This builds the extension and installs it into the active Python environment.

## Local Wheel Build

```bash
maturin build --release
```

Run `docker run --rm -it bgpkit-builder:latest bash` to open a shell in the container
Built wheels are written under `target/wheels/`.

## Manual Publish Fallback

If CI is unavailable, build locally and upload with `twine`:

```bash
bash build.sh
python -m pip install --upgrade maturin twine
maturin build --release --sdist
twine upload --skip-existing target/wheels/*
```

19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

All notable changes to this project will be documented in this file.

## 0.7.0 - TBD

### Highlights

* Update `bgpkit-parser` to v0.17.0.
* Update PyO3 to v0.28 and enable ABI3 wheels for Python 3.9+.
* Add `peer_bgp_id` and `only_to_customer` fields to `Elem`.
* Add reusable `Filter` class and `Parser.from_filters(...)` constructor.
* Add Rust-like `Elem` utility methods: `is_announcement`, `is_withdrawal`, `get_origin_asn`, `get_origin_asns`, `has_as_path`, `as_dict`, `to_json`, `to_psv`, and `get_psv_header`.
* Add `Elem.origin_asn` property and module constants `ELEM_TYPE_ANNOUNCE`, `ELEM_TYPE_WITHDRAW`, and `PSV_HEADER`.
* Add Python-native filter helper constructors: `Filter.peer_ip`, `Filter.peer_ips`, `Filter.origin_asn`, `Filter.prefix`, and `Filter.elem_type`.
* Add stream-consuming `Parser.count()` and `Parser.iter_batches(batch_size)` helpers.
* Add `RouteElem` and `RouteParser` for upstream route-level parsing (`BgpRouteElem`) and faster route identity scans.
* Add high-performance projected tuple iteration: `iter_tuples(fields)` and `iter_tuple_batches(fields, batch_size)` for `Parser` and `RouteParser`.
* Add field presets `BASIC_FIELDS`, `ROUTE_FIELDS`, and `NEXT_HOP_FIELDS`.
* Optimize `Parser.parse_all()` and batch iteration by parsing while detached from the Python interpreter before converting results into Python objects.
* Add Rust and Python benchmark scaffolding.
* Automate wheel builds and PyPI publishing via GitHub Actions.

## 0.6.2 - 2025-06-06

### Fix regressions
Expand Down
15 changes: 11 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description = "BGPKIT Parser Python Binding"
keywords = ["bgp", "mrt", "parser"]
repository = "https://github.com/bgpkit/bgpkit-parser-py"
documentation = "https://docs.rs/bgpkit-parser-py"
version = "0.6.2"
version = "0.7.0"
authors = ["Mingwei Zhang <mingwei@bgpkit.com>"]
edition = "2021"
license = "MIT"
Expand All @@ -15,10 +15,17 @@ name = "pybgpkit_parser"
crate-type = ["cdylib", "rlib"]

[dependencies]
bgpkit-parser = "0.11.1"
pyo3 = { version = "0.25", features = ["extension-module"] }
bgpkit-parser = "0.17.0"
pyo3 = { version = "0.28", features = ["extension-module", "abi3-py39"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"

[build-dependencies]
pyo3-build-config = "0.25"
pyo3-build-config = "0.28"

[dev-dependencies]
criterion = "0.5"

[[bench]]
name = "parse_bench"
harness = false
Loading
Loading