├── src └── pywrangler │ ├── py.typed │ ├── __init__.py │ ├── __main__.py │ ├── metadata.py │ ├── types.py │ ├── cli.py │ ├── utils.py │ └── sync.py ├── .gitattributes ├── workers.py ├── CLAUDE.md ├── .github └── workflows │ ├── commitlint.yml │ ├── lint.yml │ ├── tests.yml │ └── release.yml ├── .gitignore ├── README.md ├── .pre-commit-config.yaml ├── tests ├── test_types.py ├── test_py_version_detect.py └── test_cli.py ├── CONTRIBUTING.md ├── pyproject.toml ├── CHANGELOG.md └── uv.lock /src/pywrangler/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pywrangler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | uv.lock binary 2 | -------------------------------------------------------------------------------- /src/pywrangler/__main__.py: -------------------------------------------------------------------------------- 1 | from .cli import app 2 | 3 | app() 4 | -------------------------------------------------------------------------------- /workers.py: -------------------------------------------------------------------------------- 1 | # This is where the Python Workers SDK will live. 2 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | This project uses the following tools: 2 | 3 | * uv for package installation and running of tools 4 | * pytest for the implementation of tests 5 | 6 | When implementing tests, try to use features from pytest where possible. 7 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: 3 | pull_request: 4 | branches: [ main ] 5 | 6 | permissions: 7 | contents: read 8 | pull-requests: read 9 | 10 | jobs: 11 | commitlint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: wagoid/commitlint-github-action@v6 16 | -------------------------------------------------------------------------------- /src/pywrangler/metadata.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Literal, NamedTuple 3 | 4 | 5 | class PythonCompatVersion(NamedTuple): 6 | version: Literal["3.12", "3.13"] 7 | compat_flag: str 8 | compat_date: datetime | None 9 | 10 | 11 | PYTHON_COMPAT_VERSIONS = [ 12 | PythonCompatVersion( 13 | "3.13", "python_workers_20250116", datetime.strptime("2025-09-29", "%Y-%m-%d") 14 | ), 15 | PythonCompatVersion("3.12", "python_workers", None), 16 | ] 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.13' 19 | 20 | - name: Install uv 21 | run: | 22 | pip install uv 23 | 24 | - name: Run pre-commit 25 | run: | 26 | uvx pre-commit run -a 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macos-latest] 14 | python-version: ['3.12'] 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install uv 26 | run: | 27 | pip install uv 28 | 29 | - name: Install project 30 | run: | 31 | uv pip install --system -e . 32 | 33 | - name: Run tests 34 | run: | 35 | uv run pytest 36 | 37 | - name: Verify that pywrangler can be run globally 38 | run: | 39 | pywrangler --help 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | dist/ 11 | build/ 12 | *.egg-info/ 13 | .eggs/ 14 | 15 | # Virtual environments 16 | venv/ 17 | env/ 18 | ENV/ 19 | .venv/ 20 | .env/ 21 | 22 | # uv package manager 23 | .uv/ 24 | 25 | # Jupyter Notebook 26 | .ipynb_checkpoints 27 | 28 | # IDE specific files 29 | .idea/ 30 | .vscode/ 31 | *.swp 32 | *.swo 33 | 34 | # Local development settings 35 | .env 36 | .envrc 37 | 38 | # Unit test / coverage reports 39 | htmcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # mypy 52 | .mypy_cache/ 53 | .dmypy.json 54 | dmypy.json 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # pyenv 60 | .python-version 61 | 62 | # OS specific 63 | .DS_Store 64 | Thumbs.db 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # workers-py 2 | 3 | A set of libraries and tools for Python Workers. 4 | 5 | 6 | ## Pywrangler 7 | 8 | A CLI tool for managing vendored packages in a Python Workers project. 9 | 10 | ### Installation 11 | 12 | On Linux, you may be able to install the tool globally by running: 13 | 14 | ``` 15 | uv tool install workers-py 16 | ``` 17 | 18 | Alternatively, you can add `workers-py` to your pyproject.toml: 19 | 20 | ``` 21 | [dependency-groups] 22 | dev = ["workers-py"] 23 | ``` 24 | 25 | Then run via `uv run pywrangler`. 26 | 27 | ### Usage 28 | 29 | ```bash 30 | uv run pywrangler --help 31 | uv run pywrangler sync 32 | ``` 33 | 34 | ### Development 35 | 36 | To run the CLI tool while developing it, install it globally: 37 | 38 | ``` 39 | uv tool install -e . 40 | ``` 41 | 42 | Then run it via `pywrangler`. 43 | 44 | Alternatively, you can add `workers-py` to your pyproject.toml: 45 | 46 | ``` 47 | [dependency-groups] 48 | dev = ["workers-py"] 49 | 50 | [tool.uv.sources] 51 | workers-py = { path = "../workers-py" } 52 | ``` 53 | 54 | Then run via `uv run pywrangler`. 55 | 56 | #### Lint 57 | 58 | ``` 59 | uv run ruff check --fix 60 | uv run ruff format 61 | ``` 62 | 63 | #### Tests 64 | 65 | ``` 66 | $ uv cache clean 67 | $ uv run pytest 68 | $ uv run pytest tests/test_cli.py::test_sync_command_handles_missing_pyproject -v # Specific test 69 | ``` 70 | -------------------------------------------------------------------------------- /src/pywrangler/types.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | from tempfile import TemporaryDirectory 4 | 5 | from .utils import WRANGLER_COMMAND, run_command 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | TSCONFIG = """ 10 | { 11 | "compilerOptions": { 12 | "target": "esnext", 13 | "module": "esnext", 14 | "moduleResolution": "nodenext", 15 | "lib": ["esnext"] 16 | }, 17 | "include": ["worker-configuration.d.ts"] 18 | } 19 | """ 20 | 21 | PACKAGE_JSON = """ 22 | { 23 | "dependencies": { 24 | "typescript": "^5.3.2" 25 | } 26 | } 27 | """ 28 | 29 | 30 | def wrangler_types(outdir_arg: str | None, config: str | None, /) -> None: 31 | args = ["types"] 32 | if config: 33 | args += ["--config", config] 34 | if outdir_arg is None: 35 | outdir = Path("src") 36 | else: 37 | outdir = Path(outdir_arg) 38 | stubs_dir = outdir / "js-stubs" 39 | stubs_dir.mkdir(parents=True, exist_ok=True) 40 | with TemporaryDirectory() as tmp_str: 41 | tmp = Path(tmp_str) 42 | run_command(WRANGLER_COMMAND + args + [tmp / "worker-configuration.d.ts"]) 43 | (tmp / "tsconfig.json").write_text(TSCONFIG) 44 | (tmp / "package.json").write_text(PACKAGE_JSON) 45 | run_command(["npm", "-C", tmp, "install"]) 46 | run_command(["npx", "@pyodide/ts-to-python", tmp, stubs_dir / "__init__.pyi"]) 47 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: (^.*patches|.*\.cgi$|^packages/micropip/src/micropip/externals|^benchmark/benchmarks$) 2 | default_language_version: 3 | python: "3.13" 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: "v5.0.0" 7 | hooks: 8 | - id: check-added-large-files 9 | - id: check-case-conflict 10 | - id: check-merge-conflict 11 | - id: check-symlinks 12 | - id: check-yaml 13 | - id: debug-statements 14 | - id: end-of-file-fixer 15 | - id: mixed-line-ending 16 | - id: trailing-whitespace 17 | - id: requirements-txt-fixer 18 | 19 | - repo: https://github.com/astral-sh/ruff-pre-commit 20 | rev: "v0.9.1" 21 | hooks: 22 | - id: ruff 23 | args: [--fix] 24 | - id: ruff-format 25 | 26 | - repo: https://github.com/shellcheck-py/shellcheck-py 27 | rev: "v0.10.0.1" 28 | hooks: 29 | - id: shellcheck 30 | 31 | - repo: https://github.com/codespell-project/codespell 32 | rev: "v2.3.0" 33 | hooks: 34 | - id: codespell 35 | args: 36 | [ 37 | "--ignore-words-list", 38 | "ommit", 39 | ] 40 | - repo: https://github.com/pre-commit/mirrors-mypy 41 | rev: "v1.14.1" 42 | hooks: 43 | - id: mypy 44 | exclude: (.*test.*) 45 | additional_dependencies: 46 | - click 47 | ci: 48 | autoupdate_schedule: "quarterly" 49 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | from contextlib import chdir 2 | from subprocess import run 3 | 4 | # Import the full module so we can patch constants 5 | from pywrangler.types import wrangler_types 6 | 7 | WRANGLER_TOML = """ 8 | compatibility_date = "2025-08-14" 9 | 10 | kv_namespaces = [ 11 | { binding = "FOO", id = "" } 12 | ] 13 | """ 14 | 15 | PYPROJECT_TOML = """ 16 | [dependency-groups] 17 | dev = [ 18 | "mypy>=1.17.1", 19 | "pyodide-py", 20 | "workers-runtime-sdk", 21 | ] 22 | 23 | [tool.mypy] 24 | files = [ 25 | "src", 26 | ] 27 | """ 28 | 29 | WORKER = """ 30 | from workers import WorkerEntrypoint, Response, Request 31 | 32 | 33 | class Default(WorkerEntrypoint): 34 | async def fetch(self, request: Request) -> Response: 35 | reveal_type(self.env) 36 | reveal_type(self.env.FOO) 37 | await self.env.FOO.put("bar", "baz") 38 | bar = await self.env.FOO.get("bar") 39 | assert bar 40 | reveal_type(bar) 41 | return Response(bar) 42 | """ 43 | 44 | 45 | def test_types(tmp_path): 46 | config_path = tmp_path / "wrangler.toml" 47 | pyproject_path = tmp_path / "pyproject.toml" 48 | worker_dir = tmp_path / "src/worker" 49 | worker_path = worker_dir / "entry.py" 50 | 51 | worker_dir.mkdir(parents=True) 52 | worker_path.write_text(WORKER) 53 | config_path.write_text(WRANGLER_TOML) 54 | pyproject_path.write_text(PYPROJECT_TOML) 55 | 56 | with chdir(tmp_path): 57 | wrangler_types(None, None) 58 | result = run(["uv", "run", "mypy"], capture_output=True, text=True, check=False) 59 | assert 'Revealed type is "js.Env"' in result.stdout 60 | assert 'Revealed type is "js.KVNamespace_iface"' in result.stdout 61 | assert 'Revealed type is "builtins.str"' in result.stdout 62 | assert "Success: no issues found" in result.stdout 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to workers-py 2 | 3 | Thank you for your interest in contributing to workers-py! This document provides guidelines and information to help you contribute effectively. 4 | 5 | ## Table of Contents 6 | 7 | - [Getting Started](#getting-started) 8 | - [Development Setup](#development-setup) 9 | - [Making Changes](#making-changes) 10 | - [Commit Message Guidelines](#commit-message-guidelines) 11 | - [Release Process](#release-process) 12 | - [Submitting Changes](#submitting-changes) 13 | 14 | ## Getting Started 15 | 16 | 1. Fork the repository on GitHub 17 | 2. Clone your fork locally: 18 | ```bash 19 | git clone https://github.com/YOUR_USERNAME/workers-py.git 20 | cd workers-py 21 | ``` 22 | 3. Set up the development environment (see [Development Setup](#development-setup)) 23 | 24 | ## Development Setup 25 | 26 | Follow the [Development section](https://github.com/cloudflare/workers-py#development) of the README for setting up your development environment. 27 | 28 | ### Development Dependencies 29 | 30 | The project includes these development tools: 31 | - **pytest**: Testing framework 32 | - **ruff**: Fast Python linter 33 | - **mypy**: Static type checking 34 | 35 | ## Making Changes 36 | 37 | 1. Create a new branch for your feature or bugfix: 38 | ```bash 39 | git checkout -b your-username/your-change-name 40 | ``` 41 | 42 | 2. Run our code formatter via `uvx ruff format` and linter via `uvx ruff fix .` 43 | 44 | 3. Add or update tests as needed 45 | 46 | 4. Run the test suite: `uv clean cache && uv run pytest` 47 | 48 | ## Commit Message Guidelines 49 | 50 | This project uses automated semantic versioning via `python-semantic-release` which relies on 51 | tags in the commit message to determine whether a release should be made. 52 | 53 | ### Commit Format 54 | 55 | The format parsed by python-semantic-release is https://www.conventionalcommits.org/en/v1.0.0/#summary. 56 | It looks something like this: 57 | 58 | ``` 59 | (): 60 | 61 | 62 | 63 |