Metadata-Version: 2.4
Name: portly
Version: 0.1.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Rust
Classifier: Topic :: System :: Systems Administration
Classifier: Typing :: Typed
License-File: LICENSE
Summary: Cross-platform Python port management made simple
Keywords: port,tcp,network,process,sysadmin,dev-tools
Author-email: Patryk Bochenek <contact@patrykbochenek.com>
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/PatrykBochenek/portly/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/PatrykBochenek/portly
Project-URL: Issues, https://github.com/PatrykBochenek/portly/issues
Project-URL: Repository, https://github.com/PatrykBochenek/portly

<div align="center">

# portly

**Cross-platform Python port management made simple.**

[![PyPI version](https://img.shields.io/pypi/v/portly)](https://pypi.org/project/portly/)
[![Python versions](https://img.shields.io/pypi/pyversions/portly)](https://pypi.org/project/portly/)
[![License](https://img.shields.io/pypi/l/portly)](https://github.com/PatrykBochenek/portly/blob/main/LICENSE)
[![CI](https://github.com/PatrykBochenek/portly/actions/workflows/CI.yml/badge.svg)](https://github.com/PatrykBochenek/portly/actions/workflows/CI.yml)
[![Typed](https://img.shields.io/badge/typing-typed-4b8bbe)](https://github.com/PatrykBochenek/portly/blob/main/python/portly/py.typed)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)

</div>

Check whether a port is free, find a free port, wait for one to become free,
inspect what is listening on it, scan a list of ports, or kill whatever is
holding one — fast, and on Linux, macOS, and Windows.

Everything is implemented natively (no `lsof`, `netstat`, `wmic`, or other
subprocesses), so it works in minimal containers and is safe from shell
escaping issues.

## Installation

```bash
pip install portly
```

Requires Python 3.10+. Pre-built wheels are available for Linux (manylinux &
musllinux), macOS (Intel & Apple Silicon), and Windows (x64 & arm64).

## Quick Start

```python
import portly

# Check if a port is available
if portly.is_available(8000):
    print("Port 8000 is free!")

# Find a free port (preferring 8000 if it happens to be free)
port = portly.find_free(8000)

# Wait for a port to become free (up to 30s)
portly.wait_until_free(5432, timeout=30)

# Kill the process using a port
portly.kill(8000)

# Get process info
info = portly.get_info(8000)
# {'pid': 1234, 'name': 'python', 'cmd': 'python app.py'}  (or None)

# Scan multiple ports at once
results = portly.scan([8000, 8001, 5432])
```

## API Reference

| Function | Description |
| --- | --- |
| `is_available(port: int) -> bool` | `True` if the port is free on localhost |
| `find_free(preferred: int \| None = None) -> int` | Returns `preferred` if free, otherwise any free port |
| `wait_until_free(port: int, timeout: int = 30) -> bool` | `True` when the port frees up, `False` on timeout |
| `get_info(port: int) -> PortInfo \| None` | `{"pid", "name", "cmd"}` for the listener, `None` if free |
| `kill(port: int, force: bool = False) -> bool` | Terminates the process(es) using the port |
| `scan(ports: list[int]) -> dict[int, PortInfo \| None]` | Process info per port |

`PortInfo` is a `TypedDict`: `{"pid": int, "name": str, "cmd": str}`.

### Platform notes

- **`get_info`** — `cmd` is the full command line on Linux, the executable
  path on macOS, and the executable name on Windows.
- **`kill`** — sends `SIGTERM` unless `force=True` (`SIGKILL`). On Windows the
  process is always terminated forcefully. Killing another user's process
  requires elevated privileges and raises `PermissionError` otherwise.
- **`find_free`** — the returned port can be taken between the check and your
  `bind()` (TOCTOU). If you need a race-free reservation, bind to port `0` and
  let the OS choose.
- **Visibility** — process info and kills only cover processes visible to your
  user; other users' listeners may report as "free"/unreachable without
  elevated privileges (same limitation as `lsof`).

## Why portly?

- **Cross-platform** — Linux, macOS, and Windows, with per-arch wheels.
- **Fast** — Rust core; no Python socket gymnastics, no subprocess spawning.
- **Correct** — native APIs (`/proc`, `libproc`, `GetExtendedTcpTable`), with
  graceful handling of zombies, permissions, and races.
- **Typed** — first-party type stubs checked against the compiled module.
- **Small** — six functions, one dependency-free story.

## Development

See [CONTRIBUTING.md](CONTRIBUTING.md) for the full setup. Quick start:

```bash
uv sync --group dev     # install dev dependencies (or use pip)
maturin develop         # build the extension in-place
pytest tests/           # run the tests
```

Quality gates: `cargo fmt`/`clippy`/`test`, `ruff check`/`format`, `mypy`,
and `mypy.stubtest`. All are enforced in CI.

## Examples

The [`examples/`](examples/) directory contains a FastAPI playground app that
exercises every function through a small web UI:

```bash
pip install fastapi uvicorn
uvicorn examples.main:app --reload --port 8712
```

## License

[MIT](LICENSE) © Patryk Bochenek
