Metadata-Version: 2.4
Name: abstract_pypit
Version: 0.1.0
Summary: One-command PyPI publisher + GitHub pusher, and a server-wide staging release loop (init / stage / release / request / runner).
Home-page: https://github.com/AbstractEndeavors/abstract_pypit
Author: putkoff
Author-email: putkoff <partners@abstractendeavors.com>
License: MIT
Project-URL: Homepage, https://github.com/AbstractEndeavors/abstract_pypit
Keywords: pypi,publish,release,github,automation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: requests
Requires-Dist: build
Requires-Dist: twine
Dynamic: author
Dynamic: home-page
Dynamic: requires-python

# abstract_pypit

One-command PyPI publisher + GitHub pusher, and a server-wide **staging release
loop** (`init / stage / release / request / runner`).

Finds the next free version above whatever is on PyPI, bumps `setup.py` and
`pyproject.toml`, builds sdist + wheel, uploads to PyPI via twine, commits and
pushes to GitHub, then syncs the local install — all in one call.

Dependencies: `requests`, plus `build` and `twine` (so a fresh install can publish).

## Install

```sh
pip install abstract_pypit
```

## Usage

```python
# from any package directory that has setup.py + pyproject.toml:
from pypit import runit
runit()
```

```sh
# or from the command line:
abstract-pypit
```

## Staging release loop

The source of a package is a directory on the server (default
`/srv/pyit/dev/<project>`, symlinks resolved). Nobody edits it directly; changes
reach it only through a disposable staging dir and a host-side runner:

```sh
# once, as the user who owns the staging root (the "runner"):
abstract-pypit runner install --root /srv/vm_mgr/src --requesters vm_mgr,hugpy
abstract-pypit init abstract_utilities --root /srv/vm_mgr/src      # staging dir + push.sh, then stage

# any requester:
/srv/vm_mgr/src/abstract_utilities/push.sh stage     # source -> ./source -> ./live
#   ... edit ./live, test there ...
/srv/vm_mgr/src/abstract_utilities/push.sh release   # upload, then the source is swapped
/srv/vm_mgr/src/abstract_utilities/push.sh           # status
```

`push.sh` only drops `<root>/_pypit/requests/<project>.<action>.<id>.req` and follows
the log (`abstract-pypit request ...`); the owner's systemd `--user` path unit runs
`abstract-pypit runner run`, which executes requests one at a time. No ssh, sudo or
credentials are needed to release.

**release**, per project:

1. If the source moved on since staging (version, content or git HEAD), it PAUSES:
   writes `merge-*.diff` files, 3-way merges the source into `live/` (base =
   `source/`), marks conflicts `<<<<<<< live / ======= / >>>>>>> source` (or a
   `<file>.CONFLICT` note; nothing wins), rebases `source/` and stops. The merged
   live is what the next push releases; it refuses while markers or notes remain.
2. Otherwise, by backend (auto-detected):
   * **pypi**: runPypit inside `live/` (next free version, build, upload,
     commit+push without pulling, sync the local install); success = PyPI lists the
     version.
   * **pkg_src**: the source is `<repo>/py/<group>/<pkg>` of a pkg-src-watched tree
     (`<repo>/py/tooling/pkg_src.py`); the swap is the release; success =
     pkg-src-watch recorded exactly the swapped tree (read-only query), else the swap
     is reversed.
3. `live/` moves to `prod/`, `prod/` is copied to a temp dir and content-verified,
   then source -> `_old`, temp -> source, `_old` removed; then it re-stages.

Copies are `rsync -aAX --checksum`, keep `.git`, and exclude build output. Commands:

| command | |
|---|---|
| `abstract-pypit init <p> [--root R] [--source S] [--editors u1,u2] [--handoff R2] [--force]` | create/rebuild the staging dir, stage |
| `abstract-pypit stage <p> [--force]` / `release <p>` / `status <p>` | the engine (run by the runner) |
| `abstract-pypit request <stage\|release\|status> <p>` | what push.sh calls |
| `abstract-pypit runner install [--root R ...] [--requesters u1,u2]` / `run --root R` / `status` | the runner |

Env: `PYPIT_ROOT` (default root `~/staging`), `PYPIT_DEV_ROOT` (default
`/srv/pyit/dev`), `PYPIT_PYPI_WAIT`, `PYPIT_PKG_SRC_WAIT`, `PYPIT_PKG_SRC_PYTHON`.
Tests: `python -m unittest discover -s tests` (nothing is uploaded).

## Credentials

**PyPI:** twine reads `~/.pypirc` or `TWINE_USERNAME` / `TWINE_PASSWORD` env vars.

**GitHub:** create `pypit/src/envs/.env` on the machine running pypit:

```
GITHUB_OWNER_1=your-username
GITPASS_1=<your-github-pat>
GITHUB_OWNER_2=your-org
GITPASS_2=<org-github-pat>
```

SSH key at `~/.ssh/github/githubssh_nopass` must be registered with GitHub.

## Per-package config

Add to the package's own `pyproject.toml`:

```toml
[tool.pypit]
github_owner = "your-org-or-username"   # which org/user owns the repo
github_push  = true                      # set false to skip GitHub entirely
```

## License

MIT
