Metadata-Version: 2.5
Name: pullup
Version: 0.0.9
Summary: build, release, and deploy projects
Project-URL: Repository, https://github.com/vedicreader/pullup
Project-URL: Documentation, https://vedicreader.github.io/pullup
Author-email: Karthik Rajgopal <karthik.rajgopal@hotmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: jupyter,nbdev,notebook,python
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.11
Requires-Dist: cfeasy>=0.0.6
Requires-Dist: dockeasy>=0.0.10
Requires-Dist: fastcore>=1.8.17
Requires-Dist: gheasy>=0.0.10
Requires-Dist: keyring>=25.0.0
Requires-Dist: ptymini>=0.0.5
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: vpseasy>=0.0.24
Description-Content-Type: text/markdown

# pullup


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

pullup is the engine behind a release and deploy panel. Point it at a project folder and it tells you what kind of project it is (an nbdev library, a plain Python package, a Rust crate and so on) and which commands release it. It then runs those commands in a terminal you can watch, and stops at the first one that fails. The same machinery runs the deploy of a web app to a virtual private server (VPS). pullup also reads the GitHub Actions workflows in the repository, writes new ones, and lists the Hetzner servers and Cloudflare tunnels the project uses.

Every public call returns plain dictionaries and lists, so a user interface can draw them and a test can compare them. The work that talks to GitHub, Docker, Hetzner and Cloudflare is done by four sibling libraries: gheasy, dockeasy, vpseasy and cfeasy.

## Install

``` sh
pip install pullup
```

This also installs gheasy, dockeasy, vpseasy and cfeasy. None of them is imported until a call needs it, so reading a project or running a release does not load them.

## What kind of project is this

A release flow is the ordered list of commands that publishes one kind of project. An nbdev library is released with `nbdev-prepare`, `nbdev-bump-version`, `nbdev-release-gh` and `nbdev-pypi`. A plain Python package and a Rust crate each have a different list. [`Project`](https://vedicreader.github.io/pullup/project.html#project) looks at the files in a folder, picks the flow that fits, and returns its steps. The folder here holds only a `pyproject.toml` with a `[tool.nbdev]` table, so it is an nbdev project:

``` python
project = Project(root)
project.kind, [step.id for step in project.steps()]
```

    ('nbdev', ['prepare', 'bump', 'gh', 'pypi'])

## Running the steps

A [`Pipeline`](https://vedicreader.github.io/pullup/pipeline.html#pipeline) runs a list of steps for one project. Each step runs in a pseudo-terminal (PTY), so a program that asks for a password behaves as it would in your own shell, and every step’s output goes into one shared transcript. When a step passes, the next one starts by itself. When one fails, the pipeline stops there.

`save` stores the plan and `start` launches the first step that has not yet passed. `start` returns as soon as the step is running, not when it finishes, which is why the transcript below shows the command but not yet its output. Call `state` or `tail` whenever you want to see how far it has got.

``` python
pipeline = Pipeline(root, dir='.demo')
pipeline.save([{'id': 'hello', 'cmd': 'say hello',
                'argv': [sys.executable, '-c', 'print("ready")']}])
await pipeline.start()
pipeline.state()['done'], pipeline.tail().splitlines()[-2:]
```

    (False, ['', '❯ say hello'])

## Where plans are kept

A saved plan is a JSON file inside the project folder, in the directory you pass as `dir` (`.pullup` unless you say otherwise). pullup never writes a plan outside the project:

``` python
custom = Pipeline(root, dir='.myapp')
custom.save([{'id': 'show', 'cmd': 'show', 'argv': [sys.executable, '-c', 'print("saved")']}])
custom.path.relative_to(root)
```

    Path('.myapp/pipeline.json')

## Deploying a web app

[`Deploy`](https://vedicreader.github.io/pullup/deploy.html#deploy) is a pipeline that puts a web app on a server. Its steps run the subcommands of a `deploy.py` script that lives in the project, and [`Deploy.scaffold`](https://vedicreader.github.io/pullup/deploy.html#deploy.scaffold) can write that script for you. [`Drive`](https://vedicreader.github.io/pullup/drive.html#drive) covers the same ground one library call at a time (write the Dockerfile, open the tunnel, provision the server), so you can run and check each part on its own. Both are pipelines, so they start, stop and report like the one above.

``` python
Deploy(root).state()['kind'], Drive(root).state()['kind']
```

    ('deploy', 'drive')

## Workflows

[`Workflows`](https://vedicreader.github.io/pullup/workflows.html#workflows) reads the GitHub Actions files in `.github/workflows` and turns each one into a row of triggers and jobs. Reading local files needs no GitHub token. Listing runs, starting them and pushing secrets do.

``` python
workflow_dir = root/'.github'/'workflows'
workflow_dir.mkdir(parents=True)
(workflow_dir/'ci.yml').write_text('''name: CI
on: push
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: pytest
''')
workflows = Workflows(root)
[(row['name'], [job['id'] for job in row['jobs']]) for row in workflows.parsed()]
```

    [('CI', ['test'])]

## Environment values

Many steps need a token or a setting, such as `GITHUB_TOKEN` for a GitHub release. A step lists the names it needs under `needs`, and the pipeline looks them up in an environment store when it starts the step. The values are given to the child process only, and your own process environment is left alone. The default store is an [`EnvStore`](https://vedicreader.github.io/pullup/env.html#envstore), which keeps secrets in the system keychain, but any object with `get` and `values` methods will do, like the small one below:

``` python
class Values:
    def get(self, key, secret=True): return 'demo-token' if key == 'DEMO_TOKEN' else ''
    def values(self, keys): return {key: self.get(key) for key in keys if self.get(key)}

with_values = Pipeline(root, env=Values(), dir='.env-demo')
with_values.save([{'id': 'env', 'cmd': 'read environment', 'needs': ['DEMO_TOKEN'],
                   'argv': [sys.executable, '-c', 'import os; print(os.environ["DEMO_TOKEN"])']}])
await with_values.start()
with_values.tail().splitlines()[-2:]
```

    ['', '❯ read environment']

## Which package broke

When a step fails with a Python traceback, the useful question is which of your own packages raised it. [`blame`](https://vedicreader.github.io/pullup/stack.html#blame) reads the traceback text, finds the deepest frame that belongs to one of the packages named in `family`, and returns that package with the file, the line, the function and the error message:

``` python
trace = '''Traceback (most recent call last):
  File "/tmp/pullup/demo.py", line 7, in release
    raise RuntimeError("stopped")
RuntimeError: stopped
'''
blame(trace, family=['pullup'])
```

    {'package': 'pullup',
     'file': '/tmp/pullup/demo.py',
     'line': 7,
     'fn': 'release',
     'error': 'RuntimeError: stopped',
     'open': '/tmp/pullup/demo.py',
     'checkout': '',
     'version': '0.0.4'}

## Servers and tunnels

[`Infra`](https://vedicreader.github.io/pullup/infra.html#infra) lists and deletes Hetzner servers and Cloudflare tunnels, and lists Cloudflare zones and DNS records, using the tokens in the project’s environment store. `status` says which of the two libraries are installed and which tokens are set, without sending a request. An empty `env` falls back to the default [`EnvStore`](https://vedicreader.github.io/pullup/env.html#envstore), so the answer below reflects the keychain of the machine that ran it:

``` python
infra = Infra(env={})
infra.status()
```

    {'vpseasy': True,
     'cfeasy': True,
     'hcloud_token': True,
     'cf_token': False,
     'keys': ['HCLOUD_TOKEN', 'CLOUDFLARE_API_TOKEN']}
