Skip to content
Draft
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
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# v4.0

Major release bringing asyncio + AsyncSSH across the codebase.

- core: replace the gevent runtime with asyncio-powered execution helpers and staged context handling
- ssh: migrate the SSH connector to AsyncSSH (agent forwarding, retries, SFTP) while keeping sync wrappers (`connect_all`, `run_ops`, etc.)
- connectors: fold the legacy Paramiko connectors (``@scp``/``@sshuserclient``) into the upgraded AsyncSSH connector
- api: add an ``AsyncContext`` helper for running operations/facts concurrently with correct state management and new tests/docs
- cli: harden async host loading, progress handling, and exception reporting when importing inventories/operations
- docs: update references to the 4.x branch and add upgrade guidance from 3.x -> 4.x

# v3.5.2

- fix operation & fact docs generation
Expand Down Expand Up @@ -300,7 +311,7 @@ v3 of pyinfra includes for the first time a (mostly) typed internal API with pro

- Add new `_if` global argument to control operation execution at runtime
- Add `--debug-all` flag to set debug logging for all packages
- Retry SSH connections on failure (configurable, see [SSH connector](https://docs.pyinfra.com/en/3.x/connectors/ssh.html#available-data)) (@fwiesel)
- Retry SSH connections on failure (configurable, see [SSH connector](https://docs.pyinfra.com/en/4.x/connectors/ssh.html#available-data)) (@fwiesel)
- Documentation typo fixes (@szepeviktor, @sudoBash418)
- Fix handling of binary files in Docker connector (@matthijskooijman)
- Add `will_change` attribute and `did_change` context manager to `OperationMeta`
Expand Down
60 changes: 60 additions & 0 deletions docs/api/async_context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# AsyncContext helper

`pyinfra.async_context.AsyncContext` gives you a small async-friendly wrapper to
run individual operations and facts without executing a full deploy. It reuses
the existing `State` object, so the same inventory/config settings apply.

```python
import asyncio

from pyinfra.api import Config, State
from pyinfra.async_context import AsyncContext
from pyinfra.facts.server import Hostname
from pyinfra.operations import server


async def main() -> None:
inventory = ... # construct your Inventory
state = State(inventory, Config())

async with AsyncContext(state) as ctx:
await server.shell("echo hello from async")
facts = await ctx.get_fact(Hostname)
print(f"Hostnames: {facts}")


asyncio.run(main())
```

`AsyncContext` is an async context manager and will automatically connect to the
target hosts on entry and disconnect them when the block exits. While inside the
context you can call operations directly (for example `await server.shell(...)`);
the operation will execute immediately for the selected hosts and return a
mapping of host to `OperationMeta`. If you only want to target a subset of hosts
you can pass them via the `hosts=` parameter when creating the context or when
calling `server.shell(..., hosts=[...])`/`ctx.get_fact(..., hosts=[...])`.

## Synchronous helper

For synchronous code you can use `pyinfra.sync_context.SyncContext`, which
exposes the same API without the asyncio wrappers:

```python
from pyinfra.api import Config, State
from pyinfra.facts.server import Hostname
from pyinfra.operations import server
from pyinfra.sync_context import SyncContext


def main() -> None:
inventory = ...
state = State(inventory, Config())

with SyncContext(state) as ctx:
server.shell("echo hello from sync")
facts = ctx.get_fact(Hostname)
print(f"Hostnames: {facts}")


main()
```
18 changes: 12 additions & 6 deletions docs/api/index.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
Using the API
=============

In addition to :doc:`the pyinfra CLI <../cli>`, pyinfra provides a full Python API. As of ``v3`` this API can be considered mostly stable. See the :doc:`./reference`.
In addition to :doc:`the pyinfra CLI <../cli>`, pyinfra provides a full Python API. As of ``v4`` this API can be considered mostly stable. See the :doc:`./reference`.

You can also reference `pyinfra's own main.py <https://github.com/pyinfra-dev/pyinfra/blob/3.x/pyinfra_cli/main.py>`_, and the `pyinfra API source code <https://github.com/pyinfra-dev/pyinfra/tree/3.x/pyinfra/api>`_.
You can also reference `pyinfra's own main.py <https://github.com/pyinfra-dev/pyinfra/blob/4.x/pyinfra_cli/main.py>`_, and the `pyinfra API source code <https://github.com/pyinfra-dev/pyinfra/tree/4.x/pyinfra/api>`_.

Context Helpers
---------------

Async and sync helpers for running individual operations and facts are
documented in :doc:`./async_context`.

Full Example
------------
Expand All @@ -24,15 +30,15 @@ Basic Localhost Example
from pyinfra.operations import server

# Define your inventory (@local means execute on localhost using subprocess)
# https://docs.pyinfra.com/en/3.x/apidoc/pyinfra.api.inventory.html
# https://docs.pyinfra.com/en/4.x/apidoc/pyinfra.api.inventory.html
inventory = Inventory((["@local"], {}))

# Define any config you need
# https://docs.pyinfra.com/en/3.x/apidoc/pyinfra.api.config.html
# https://docs.pyinfra.com/en/4.x/apidoc/pyinfra.api.config.html
config = Config(SUDO=True)

# Set up the state object
# https://docs.pyinfra.com/en/3.x/apidoc/pyinfra.api.state.html
# https://docs.pyinfra.com/en/4.x/apidoc/pyinfra.api.state.html
state = State(inventory=inventory, config=config)

# Connect to all the hosts
Expand Down Expand Up @@ -62,5 +68,5 @@ Basic Localhost Example
print(result2.changed, result2[host].stdout, result2[host].stderr)

# We can also get facts for all the hosts
# https://docs.pyinfra.com/en/3.x/apidoc/pyinfra.api.facts.html
# https://docs.pyinfra.com/en/4.x/apidoc/pyinfra.api.facts.html
print(get_facts(state, Os))
2 changes: 1 addition & 1 deletion docs/api/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The pyinfra API is designed to be used as follows:
3. Now that's done, we execute it:
- ``pyinfra.api.operations.run_ops``

Currently the best example of this in action is in `pyinfra's own main.py <https://github.com/pyinfra-dev/pyinfra/blob/3.x/pyinfra_cli/main.py>`_.
Currently the best example of this in action is in `pyinfra's own main.py <https://github.com/pyinfra-dev/pyinfra/blob/4.x/pyinfra_cli/main.py>`_.

.. toctree::
:caption: Core API
Expand Down
9 changes: 8 additions & 1 deletion docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pyinfra works on anywhere that runs Python - Mac, Linux & Windows are all suppor

#### PyCharm

To debug pyinfra within PyCharm, you need to [explicitly enable support for Gevent](https://blog.jetbrains.com/pycharm/2012/08/gevent-debug-support/).
When debugging pyinfra within PyCharm, enable [asyncio debug support](https://www.jetbrains.com/help/pycharm/debugging-asynchronous-code.html) to inspect tasks and breakpoints inside the event loop.


## Remote Systems
Expand All @@ -45,6 +45,13 @@ pyinfra aims to be compatible with all Unix-like operating systems and is curren
In general, the only requirement on the remote side is shell access. POSIX commands are used where possible for facts and operations, so most of the ``server`` and ``files`` operations should work anywhere POSIX.


## Upgrading pyinfra from ``3.x`` -> ``4.x``

- Core execution now uses Python ``asyncio`` instead of gevent. The synchronous helpers remain, but custom tooling which previously interacted with gevent greenlets may need to switch to the new async APIs.
- SSH connectivity is powered by AsyncSSH. Paramiko-specific connection kwargs such as ``ssh_paramiko_connect_kwargs`` are no longer supported.
- The legacy ``@scp`` and ``@sshuserclient`` connectors have been removed. Their behaviour is covered by the updated ``@ssh`` connector which uses AsyncSSH’s SFTP implementation.


## Upgrading pyinfra from ``2.x`` -> ``3.x``

- Rename `_use_sudo_password` argument to `_sudo_password`
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
"docsearch_index_name": "pyinfra",
"plausible_domain": "docs.pyinfra.com",
"plausible_stats_domain": "stats.oxygem.com",
"doc_versions": ["3.x", "2.x", "1.x", "0.x", "latest"],
"primary_doc_version": "3.x",
"doc_versions": ["4.x", "3.x", "2.x", "1.x", "0.x", "latest"],
"primary_doc_version": "4.x",
}

myst_heading_anchors = 3
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Third party pull requests help expand pyinfra's functionality and are essential

## Branches

+ There is a branch per major version, ie `3.x`, that tracks the latest release of that version
+ There is a branch per major version, ie `4.x`, that tracks the latest release of that version
+ Changes should generally be based off the latest major branch, unless fixing an old version

## Dev Setup
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
pyinfra Documentation
=========================

Welcome to the pyinfra v3 documentation. If you're new to pyinfra you should start with the :doc:`getting-started` page.
Welcome to the pyinfra v4 documentation. If you're new to pyinfra you should start with the :doc:`getting-started` page.


Using pyinfra
Expand Down
15 changes: 4 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
[project]
name = "pyinfra"
dynamic = [
"version",
]
version = "4.0.0"
description = "pyinfra automates/provisions/manages/deploys infrastructure."
readme = "README.md"
authors = [
Expand All @@ -12,8 +10,7 @@ license = "MIT"
license-files = ["LICENSE.md"]
requires-python = ">=3.10,<4.0"
dependencies = [
"gevent>=1.5",
"paramiko>=2.7,<4", # 2.7 (2019) adds OpenSSH key format + Match SSH config
"asyncssh>=2.13",
"click>2",
"jinja2>3,<4",
"python-dateutil>2,<3",
Expand Down Expand Up @@ -54,7 +51,6 @@ test = [
"pyyaml>=6.0.2,<7",
"mypy==1.17.1",
"types-cryptography>=3.3.23.2,<4",
"types-paramiko>=2.7,<4",
"types-python-dateutil>2,<3",
"types-PyYAML>6,<7",
"ruff>=0.13.1",
Expand Down Expand Up @@ -95,15 +91,12 @@ terraform = "pyinfra.connectors.terraform:TerraformInventoryConnector"
vagrant = "pyinfra.connectors.vagrant:VagrantInventoryConnector"

[build-system]
requires = ["hatchling", "uv-dynamic-versioning"]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/pyinfra", "src/pyinfra_cli"]

[tool.hatch.version]
source = "uv-dynamic-versioning"

[tool.ruff]
line-length = 100

Expand Down Expand Up @@ -131,7 +124,7 @@ markers = [
]

[tool.coverage.run]
concurrency = ["gevent"]
concurrency = ["thread"]

[tool.coverage.report]
show_missing = true
Expand Down
4 changes: 2 additions & 2 deletions scripts/build-public-docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
set -euo pipefail

# Generates /en/next
NEXT_BRANCH="3.x"
NEXT_BRANCH="4.x"
# Generates /en/latest AND redirects /page -> /en/$NAME
LATEST_BRANCH="3.x"
LATEST_BRANCH="4.x"


BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
Expand Down
3 changes: 3 additions & 0 deletions src/pyinfra/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# Setup package level version
from .version import __version__ # noqa

from .async_context import AsyncContext as AsyncContext # noqa: E402,F401
from .sync_context import SyncContext as SyncContext # noqa: E402,F401

# Initialise base classes - this sets the context modules to point at the underlying
# class objects (Host, etc), which makes ipython/etc work as expected.
init_base_classes()
8 changes: 3 additions & 5 deletions src/pyinfra/api/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from string import Formatter
from typing import IO, TYPE_CHECKING, Callable, Union

import gevent
from typing_extensions import Unpack, override

from pyinfra.context import LocalContextObject, ctx_config, ctx_host
Expand Down Expand Up @@ -236,8 +235,8 @@ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorAr
if "state" in argspec.args and "host" in argspec.args:
return self.function(state, host, *self.args, **self.kwargs)

# If we're already running inside a greenlet (ie a nested callback) just execute the func
# without any gevent.spawn which will break the local host object.
# If we're already running inside a nested callback just execute the function directly to
# avoid resetting the local host context.
if isinstance(host, LocalContextObject):
self.function(*self.args, **self.kwargs)
return
Expand All @@ -247,8 +246,7 @@ def execute_function() -> None:
with ctx_host.use(host):
self.function(*self.args, **self.kwargs)

greenlet = gevent.spawn(execute_function)
return greenlet.get()
return execute_function()


class RsyncCommand(PyinfraCommand):
Expand Down
Loading
Loading