safexml 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Export standard toolchain paths if not in PATH
5
+ if [ -n "$USERPROFILE" ]; then
6
+ export PATH="$USERPROFILE/.rustup/toolchains/stable-x86_64-pc-windows-msvc/bin:$USERPROFILE/.cargo/bin:$PATH"
7
+ fi
8
+ if [ -n "$HOME" ]; then
9
+ export PATH="$HOME/.cargo/bin:$PATH"
10
+ fi
11
+
12
+ RED='\033[0;31m'
13
+ GREEN='\033[0;32m'
14
+ YELLOW='\033[1;33m'
15
+ BLUE='\033[0;34m'
16
+ NC='\033[0m'
17
+
18
+ echo -e "${BLUE}[git-hook: pre-commit]${NC} Running pre-commit validation checks..."
19
+
20
+ # 1. Multi-Manifest Version Parity Check
21
+ echo -e "${BLUE}[1/3] Checking version parity across manifests...${NC}"
22
+ if command -v uv >/dev/null 2>&1; then
23
+ uv run python scripts/sync_version.py --check || {
24
+ echo -e "${RED}[FAIL] Version parity check failed! Run 'python scripts/sync_version.py --set <version>' to fix.${NC}"
25
+ exit 1
26
+ }
27
+ echo -e "${GREEN}[OK] Version parity confirmed.${NC}"
28
+ elif command -v python3 >/dev/null 2>&1; then
29
+ python3 scripts/sync_version.py --check || exit 1
30
+ echo -e "${GREEN}[OK] Version parity confirmed.${NC}"
31
+ elif command -v python >/dev/null 2>&1; then
32
+ python scripts/sync_version.py --check || exit 1
33
+ echo -e "${GREEN}[OK] Version parity confirmed.${NC}"
34
+ else
35
+ echo -e "${YELLOW}[SKIP] Python/uv not found; skipping version check.${NC}"
36
+ fi
37
+
38
+ # 2. Rust Code Formatting Check
39
+ echo -e "${BLUE}[2/3] Checking Rust code formatting (cargo fmt)...${NC}"
40
+ if command -v cargo >/dev/null 2>&1; then
41
+ cargo fmt --all -- --check || {
42
+ echo -e "${RED}[FAIL] Rust code formatting check failed! Run 'cargo fmt --all' to fix.${NC}"
43
+ exit 1
44
+ }
45
+ echo -e "${GREEN}[OK] Rust formatting clean.${NC}"
46
+ else
47
+ echo -e "${YELLOW}[SKIP] Cargo not found in PATH; skipping cargo fmt.${NC}"
48
+ fi
49
+
50
+ # 3. Python Linting & Formatting (Ruff)
51
+ echo -e "${BLUE}[3/3] Checking Python code quality (ruff)...${NC}"
52
+ if command -v uvx >/dev/null 2>&1; then
53
+ uvx ruff check python tests || {
54
+ echo -e "${RED}[FAIL] Ruff lint failed! Run 'uvx ruff check --fix' to auto-fix.${NC}"
55
+ exit 1
56
+ }
57
+ uvx ruff format --check python tests || {
58
+ echo -e "${RED}[FAIL] Ruff format failed! Run 'uvx ruff format' to auto-format.${NC}"
59
+ exit 1
60
+ }
61
+ echo -e "${GREEN}[OK] Python code quality clean.${NC}"
62
+ elif command -v ruff >/dev/null 2>&1; then
63
+ ruff check python tests || exit 1
64
+ ruff format --check python tests || exit 1
65
+ echo -e "${GREEN}[OK] Python code quality clean.${NC}"
66
+ else
67
+ echo -e "${YELLOW}[SKIP] Ruff not found; skipping python linting.${NC}"
68
+ fi
69
+
70
+ echo -e "${GREEN}[git-hook: pre-commit] All pre-commit checks passed successfully!${NC}"
71
+ exit 0
72
+
@@ -0,0 +1,41 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ if [ -n "$USERPROFILE" ]; then
5
+ export PATH="$USERPROFILE/.rustup/toolchains/stable-x86_64-pc-windows-msvc/bin:$USERPROFILE/.cargo/bin:$PATH"
6
+ fi
7
+ if [ -n "$HOME" ]; then
8
+ export PATH="$HOME/.cargo/bin:$PATH"
9
+ fi
10
+
11
+ RED='\033[0;31m'
12
+ GREEN='\033[0;32m'
13
+ BLUE='\033[0;34m'
14
+ NC='\033[0m'
15
+
16
+ echo -e "${BLUE}[git-hook: pre-push]${NC} Running pre-push verification tests..."
17
+
18
+ # 1. Rust Cargo Check & Tests
19
+ echo -e "${BLUE}[1/2] Running Rust tests (cargo test)...${NC}"
20
+ cargo test || {
21
+ echo -e "${RED}[FAIL] cargo test failed!${NC}"
22
+ exit 1
23
+ }
24
+ echo -e "${GREEN}[OK] Cargo tests passed.${NC}"
25
+
26
+ # 2. Python Tests
27
+ echo -e "${BLUE}[2/2] Running Python tests (pytest)...${NC}"
28
+ if [ -f ".venv/bin/pytest" ]; then
29
+ .venv/bin/pytest || {
30
+ echo -e "${RED}[FAIL] pytest failed!${NC}"
31
+ exit 1
32
+ }
33
+ echo -e "${GREEN}[OK] Pytest passed.${NC}"
34
+ elif command -v pytest >/dev/null 2>&1; then
35
+ pytest || exit 1
36
+ echo -e "${GREEN}[OK] Pytest passed.${NC}"
37
+ fi
38
+
39
+ echo -e "${GREEN}[git-hook: pre-push] All tests passed! Ready to push.${NC}"
40
+ exit 0
41
+
@@ -0,0 +1,88 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ rust-check:
19
+ name: Rust Checks (fmt, clippy, test)
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - name: Setup Rust toolchain
25
+ uses: dtolnay/rust-toolchain@stable
26
+ with:
27
+ components: rustfmt, clippy
28
+
29
+ - name: Rust Cache
30
+ uses: Swatinem/rust-cache@v2
31
+
32
+ - name: Check formatting
33
+ run: cargo fmt --all -- --check
34
+
35
+ - name: Run clippy
36
+ run: cargo clippy --all-targets -- -D warnings
37
+
38
+ - name: Run Rust tests
39
+ run: cargo test --all-targets
40
+
41
+ python-test:
42
+ name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
43
+ needs: rust-check
44
+ runs-on: ${{ matrix.os }}
45
+ strategy:
46
+ fail-fast: false
47
+ matrix:
48
+ os: [ubuntu-latest, macos-latest, windows-latest]
49
+ python-version: ["3.12", "3.13"]
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+
53
+ - name: Setup Python ${{ matrix.python-version }}
54
+ uses: actions/setup-python@v5
55
+ with:
56
+ python-version: ${{ matrix.python-version }}
57
+
58
+ - name: Setup Rust toolchain
59
+ uses: dtolnay/rust-toolchain@stable
60
+
61
+ - name: Rust Cache
62
+ uses: Swatinem/rust-cache@v2
63
+
64
+ - name: Setup uv
65
+ uses: astral-sh/setup-uv@v5
66
+
67
+ - name: Install dependencies and build safexml
68
+ run: uv pip install --system -e ".[dev]"
69
+
70
+ - name: Lint Python with Ruff
71
+ run: |
72
+ ruff check python tests
73
+ ruff format --check python tests
74
+
75
+ - name: Run pytest
76
+ run: pytest -v
77
+
78
+ version-parity:
79
+ name: Version Parity Check
80
+ runs-on: ubuntu-latest
81
+ steps:
82
+ - uses: actions/checkout@v4
83
+ - uses: actions/setup-python@v5
84
+ with:
85
+ python-version: "3.12"
86
+ - name: Verify Cargo.toml and pyproject.toml versions match
87
+ run: python scripts/sync_version.py --check
88
+
@@ -0,0 +1,161 @@
1
+ name: Release Python Wheels (PyPI)
2
+
3
+ concurrency:
4
+ group: release-pypi-${{ github.ref_name || github.ref }}
5
+ cancel-in-progress: false
6
+
7
+ on:
8
+ push:
9
+ tags:
10
+ - 'v*'
11
+ release:
12
+ types: [published]
13
+ workflow_dispatch:
14
+
15
+ permissions:
16
+ contents: write
17
+ id-token: write
18
+
19
+ jobs:
20
+ linux:
21
+ name: Build Linux Wheels (${{ matrix.target }})
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ matrix:
25
+ target: [x86_64, aarch64]
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.12"
31
+ - name: Sync Tag Version Across Manifests
32
+ shell: bash
33
+ run: |
34
+ if [[ "${{ github.ref }}" == refs/tags/* ]]; then
35
+ TAG_VER="${GITHUB_REF_NAME#v}"
36
+ echo "Synchronizing version from tag to: $TAG_VER"
37
+ python scripts/sync_version.py --set "$TAG_VER"
38
+ fi
39
+ - name: Build Wheels
40
+ uses: PyO3/maturin-action@v1
41
+ with:
42
+ target: ${{ matrix.target }}
43
+ args: --release --out dist
44
+ sccache: 'true'
45
+ manylinux: auto
46
+ - name: Upload Wheels
47
+ uses: actions/upload-artifact@v4
48
+ with:
49
+ name: wheels-linux-${{ matrix.target }}
50
+ path: dist
51
+
52
+ windows:
53
+ name: Build Windows Wheels (${{ matrix.target }})
54
+ runs-on: windows-latest
55
+ strategy:
56
+ matrix:
57
+ target: [x64]
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+ - uses: actions/setup-python@v5
61
+ with:
62
+ python-version: "3.12"
63
+ - name: Sync Tag Version Across Manifests
64
+ shell: bash
65
+ run: |
66
+ if [[ "${{ github.ref }}" == refs/tags/* ]]; then
67
+ TAG_VER="${GITHUB_REF_NAME#v}"
68
+ echo "Synchronizing version from tag to: $TAG_VER"
69
+ python scripts/sync_version.py --set "$TAG_VER"
70
+ fi
71
+ - name: Build Wheels
72
+ uses: PyO3/maturin-action@v1
73
+ with:
74
+ target: ${{ matrix.target }}
75
+ args: --release --out dist
76
+ sccache: 'true'
77
+ - name: Upload Wheels
78
+ uses: actions/upload-artifact@v4
79
+ with:
80
+ name: wheels-windows-${{ matrix.target }}
81
+ path: dist
82
+
83
+ macos:
84
+ name: Build macOS Wheels (${{ matrix.target }})
85
+ runs-on: macos-14
86
+ strategy:
87
+ matrix:
88
+ target: [x86_64-apple-darwin, aarch64-apple-darwin]
89
+ steps:
90
+ - uses: actions/checkout@v4
91
+ - uses: actions/setup-python@v5
92
+ with:
93
+ python-version: "3.12"
94
+ - name: Sync Tag Version Across Manifests
95
+ shell: bash
96
+ run: |
97
+ if [[ "${{ github.ref }}" == refs/tags/* ]]; then
98
+ TAG_VER="${GITHUB_REF_NAME#v}"
99
+ echo "Synchronizing version from tag to: $TAG_VER"
100
+ python scripts/sync_version.py --set "$TAG_VER"
101
+ fi
102
+ - name: Build Wheels
103
+ uses: PyO3/maturin-action@v1
104
+ with:
105
+ target: ${{ matrix.target }}
106
+ args: --release --out dist
107
+ sccache: 'true'
108
+ - name: Upload Wheels
109
+ uses: actions/upload-artifact@v4
110
+ with:
111
+ name: wheels-macos-${{ matrix.target }}
112
+ path: dist
113
+
114
+ sdist:
115
+ name: Build Source Distribution
116
+ runs-on: ubuntu-latest
117
+ steps:
118
+ - uses: actions/checkout@v4
119
+ - uses: actions/setup-python@v5
120
+ with:
121
+ python-version: "3.12"
122
+ - name: Sync Tag Version Across Manifests
123
+ shell: bash
124
+ run: |
125
+ if [[ "${{ github.ref }}" == refs/tags/* ]]; then
126
+ TAG_VER="${GITHUB_REF_NAME#v}"
127
+ echo "Synchronizing version from tag to: $TAG_VER"
128
+ python scripts/sync_version.py --set "$TAG_VER"
129
+ fi
130
+ - name: Build Source Distribution
131
+ uses: PyO3/maturin-action@v1
132
+ with:
133
+ command: sdist
134
+ args: --out dist
135
+ - name: Upload sdist
136
+ uses: actions/upload-artifact@v4
137
+ with:
138
+ name: wheels-sdist
139
+ path: dist
140
+
141
+ publish:
142
+ name: Publish to PyPI
143
+ needs: [linux, windows, macos, sdist]
144
+ runs-on: ubuntu-latest
145
+ environment:
146
+ name: pypi
147
+ url: https://pypi.org/p/safexml
148
+ steps:
149
+ - name: Download all artifacts
150
+ uses: actions/download-artifact@v4
151
+ with:
152
+ path: dist
153
+ pattern: wheels-*
154
+ merge-multiple: true
155
+
156
+ - name: Publish package distributions to PyPI
157
+ uses: pypa/gh-action-pypi-publish@release/v1
158
+ with:
159
+ packages-dir: dist/
160
+ skip-existing: true
161
+
@@ -0,0 +1,28 @@
1
+ # Rust
2
+ target/
3
+ **/*.rs.bk
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .venv/
11
+ venv/
12
+ dist/
13
+ build/
14
+ uv.lock
15
+ *.egg-info/
16
+ .eggs/
17
+
18
+ # IDE & Editor
19
+ .vscode/
20
+ .idea/
21
+ *.swp
22
+ *.swo
23
+
24
+ # Testing & Coverage
25
+ .pytest_cache/
26
+ .coverage
27
+ htmlcov/
28
+
@@ -0,0 +1,38 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v4.6.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ args: ['--unsafe']
9
+ - id: check-toml
10
+ - id: check-added-large-files
11
+ args: ['--maxkb=500']
12
+ - id: check-merge-conflict
13
+
14
+ - repo: https://github.com/astral-sh/ruff-pre-commit
15
+ rev: v0.6.9
16
+ hooks:
17
+ - id: ruff
18
+ args: [--fix]
19
+ files: ^(python|tests)/
20
+ - id: ruff-format
21
+ files: ^(python|tests)/
22
+
23
+ - repo: local
24
+ hooks:
25
+ - id: version-parity
26
+ name: Version parity check (Cargo.toml vs pyproject.toml)
27
+ entry: python scripts/sync_version.py --check
28
+ language: system
29
+ pass_filenames: false
30
+ files: ^(Cargo\.toml|pyproject\.toml)$
31
+
32
+ - id: cargo-fmt
33
+ name: Rust format check (cargo fmt)
34
+ entry: cargo fmt --all -- --check
35
+ language: system
36
+ pass_filenames: false
37
+ files: \.rs$
38
+
@@ -0,0 +1,43 @@
1
+ {
2
+ "tagFormat": "v${version}",
3
+ "branches": [
4
+ "main"
5
+ ],
6
+ "plugins": [
7
+ "@semantic-release/commit-analyzer",
8
+ "@semantic-release/release-notes-generator",
9
+ [
10
+ "@semantic-release/changelog",
11
+ {
12
+ "changelogFile": "CHANGELOG.md"
13
+ }
14
+ ],
15
+ [
16
+ "@semantic-release/exec",
17
+ {
18
+ "prepareCmd": "python3 scripts/sync_version.py --set ${nextRelease.version} && cargo check"
19
+ }
20
+ ],
21
+ [
22
+ "@semantic-release/git",
23
+ {
24
+ "assets": [
25
+ "CHANGELOG.md",
26
+ "Cargo.toml",
27
+ "Cargo.lock",
28
+ "pyproject.toml"
29
+ ],
30
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
31
+ }
32
+ ],
33
+ [
34
+ "@semantic-release/github",
35
+ {
36
+ "successComment": false,
37
+ "failComment": false,
38
+ "releasedLabels": false
39
+ }
40
+ ]
41
+ ]
42
+ }
43
+
@@ -0,0 +1,97 @@
1
+ # AGENTS.md
2
+
3
+ Instructions and guidelines for AI coding assistants working in the `safexml` repository.
4
+
5
+ ---
6
+
7
+ ## 1. Project Overview
8
+
9
+ `safexml` is a high-performance, secure, modern XML parser and drop-in `defusedxml` replacement built in Rust.
10
+
11
+ - **Technology**: Rust 2021, PyO3 (`abi3-py312`), `quick-xml` (0.42+), `maturin`.
12
+ - **Repository**: `polyxml/safexml`
13
+ - **Supported Python**: `Python >= 3.12` exclusively. No legacy Python 2 or Python 3.7–3.11 backward-compatibility shims.
14
+ - **Maintainer**: Bailey Nguyen (`bailey.tan.nguyen@gmail.com`).
15
+ - **Organization**: [PolyXML](https://github.com/polyxml).
16
+
17
+ ---
18
+
19
+ ## 2. Core Architectural Decisions & Invariants
20
+
21
+ When contributing or refactoring, strictly maintain the following invariants:
22
+
23
+ 1. **Python >= 3.12 Exclusively**:
24
+ - Never introduce Python 2 or legacy Python 3 (< 3.12) compatibility shims (e.g. `sys.version_info < (3, ...)` or `six`).
25
+ - Use modern syntax (`X | Y` type unions, PEP 561 typing, built-in generics).
26
+
27
+ 2. **Drop-in Compatibility with `defusedxml.ElementTree` & `xml.etree.ElementTree`**:
28
+ - Returns standard `xml.etree.ElementTree.Element` instances to maintain 100% compatibility with libraries like `pysaml2`, `python3-saml`, `openpyxl`, `python-docx`, `feedparser`, `botocore`.
29
+ - Re-exports standard ElementTree symbols (`Element`, `SubElement`, `Comment`, `PI`, `indent`, `register_namespace`, `dump`, `tostring`, `tostringlist`).
30
+ - Signature parity: `fromstring(text, parser=None, forbid_dtd=False, forbid_entities=True, forbid_external=True, ...)` must accept `parser` keyword argument (resolving `tiran/defusedxml#78`).
31
+
32
+ 3. **True GIL Detachment in Rust**:
33
+ - All tokenization, XML scanning, DTD validation, and entity checking in Rust must execute outside the Python GIL using `py.detach(|| ...)` to allow multi-threaded Python servers (FastAPI, Django, Celery, gunicorn) to parse XML concurrently without blocking.
34
+
35
+ 4. **Security by Default**:
36
+ - **Billion Laughs / Entity Expansion**: Reject non-predefined entities when `forbid_entities=True` (`EntitiesForbidden`).
37
+ - **XXE (XML External Entity)**: Reject `SYSTEM` and `PUBLIC` references when `forbid_external=True` (`ExternalReferenceForbidden`).
38
+ - **DTD Injections**: Reject `<!DOCTYPE>` declarations when `forbid_dtd=True` (`DTDForbidden`).
39
+ - **Nesting Depth Limit**: Enforce `max_depth` (default 1,000) to protect against stack exhaustion.
40
+ - **Size Limits**: Enforce `max_text_size` and `max_entity_size` to prevent memory exhaustion attacks.
41
+
42
+ 5. **Upstream Issue Fixes**:
43
+ - Maintain the fixes for tracked upstream issues:
44
+ - `tiran/defusedxml#105`: Exception formatting for `rich` tracebacks.
45
+ - `tiran/defusedxml#104`: PEP 561 `py.typed` marker and full type annotations.
46
+ - `tiran/defusedxml#87`: Standard `ElementTree.indent()` support.
47
+ - `tiran/defusedxml#80`: Direct `Element` class export.
48
+ - `tiran/defusedxml#79`: `register_namespace()` support.
49
+ - `tiran/defusedxml#76`, `#77`, `#88`: Safe `defuse_stdlib()` without breaking 3rd-party libs.
50
+
51
+ ---
52
+
53
+ ## 3. Tooling & Development Workflow
54
+
55
+ ### Rust Toolchain
56
+ - Check workspace:
57
+ ```bash
58
+ cargo check
59
+ ```
60
+ - Run Rust tests:
61
+ ```bash
62
+ cargo test
63
+ ```
64
+ - Format code:
65
+ ```bash
66
+ cargo fmt --all
67
+ ```
68
+ - Clippy:
69
+ ```bash
70
+ cargo clippy -- -D warnings
71
+ ```
72
+
73
+ ### Python Development
74
+ - Compile extension into local virtualenv:
75
+ ```bash
76
+ maturin develop
77
+ ```
78
+ - Run test suite:
79
+ ```bash
80
+ pytest -v
81
+ ```
82
+ - Lint & format with Ruff:
83
+ ```bash
84
+ ruff check python tests
85
+ ruff format python tests
86
+ ```
87
+ - Version parity check:
88
+ ```bash
89
+ python scripts/sync_version.py --check
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 4. Release Process
95
+ - Releases are automated via GitHub Actions (`.github/workflows/release.yml`) using Semantic Release (`.releaserc`) and PyPI Trusted Publishing.
96
+ - Manifest versions (`Cargo.toml` and `pyproject.toml`) are kept in sync via `scripts/sync_version.py`.
97
+
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
5
+
6
+ ## [1.0.0] - 2026-10-11
7
+
8
+ ### Features
9
+ - Initial release of `safexml`: High-performance, secure, modern XML parser and drop-in `defusedxml` replacement built in Rust.
10
+ - Core Rust streaming XML parser powered by `quick-xml` and `PyO3` with true GIL release (`py.detach`).
11
+ - Full drop-in parity for `defusedxml.ElementTree` and `xml.etree.ElementTree`.
12
+ - Strict security against Billion Laughs (exponential entity expansion), Quadratic Blowup, XXE, cyclic entities, and external DTDs.
13
+ - Enforced depth recursion limits (default 1,000) and text/entity memory bounds.
14
+ - Resolved upstream `defusedxml` open issues:
15
+ - tiran/defusedxml#105: Rich traceback exception formatting.
16
+ - tiran/defusedxml#104: PEP 561 `py.typed` marker and full typing annotations.
17
+ - tiran/defusedxml#87: Standard `ElementTree.indent()` support.
18
+ - tiran/defusedxml#80: `Element` class export.
19
+ - tiran/defusedxml#79: `register_namespace()` helper.
20
+ - tiran/defusedxml#78: `fromstring(..., parser=None)` keyword signature parity.
21
+ - tiran/defusedxml#76, #77, #88: Safe non-destructive `defuse_stdlib()`.
22
+