Metadata-Version: 2.4
Name: pydep-service
Version: 0.1.0
Summary: Deploy FastAPI applications as persistent NSSM Windows Services
Author: Zubair Jamil
License-Expression: MIT
Keywords: deployment,fastapi,nssm,service,system-administration,uvicorn,windows
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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: Topic :: System :: Installation/Setup
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# pydep

Deploy a Python/FastAPI application from its existing `.venv` as a persistent Windows Service, managed by NSSM. Standard-library-only CLI, Python 3.10 or later.

This package includes implementation and automated behavior tests. The tests use
a service double, so actual SCM/NSSM integration, account permissions, and
reboot behavior must still be verified on Windows using the procedure below.
Passing the automated suite is not a claim that those integration checks ran.

## 1. Install NSSM

1. Download NSSM from its [official site](https://nssm.cc/download). Use a build compatible with your Windows version, with command-line configuration and online log rotation support (2.24 or later).
2. Extract the appropriate architecture's `nssm.exe` into a permanent, administrator-controlled location, for example `C:\Tools\nssm\nssm.exe`.
3. Add `C:\Tools\nssm` to your Windows PATH through **System Properties > Environment Variables**, then open a new PowerShell window.
4. Verify discovery:

```powershell
Get-Command nssm.exe
nssm.exe version
```

Alternatively, supply `--nssm 'C:\Tools\nssm\nssm.exe'` to deploy/start/stop/remove. `list` queries Windows directly and `logs` reads files, so neither needs NSSM on PATH.

Keep the NSSM executable at the same location for the lifetime of its services. pydep does not download, redistribute, or automatically upgrade NSSM. See the official [usage guide](https://nssm.cc/usage).

## 2. Install pydep

The package published on PyPI is named `pydep-service`, while the installed
command remains `pydep`. The recommended installation uses uv's isolated tool
environment:

```powershell
uv tool install pydep-service
pydep --version
pydep --help
```

Run it once without installing it permanently:

```powershell
uvx --from pydep-service pydep --help
```

Alternative installation methods are:

```powershell
pipx install pydep-service
py -m pip install pydep-service
```

Install the CLI into a stable, isolated tool environment or Python
installation, not an application venv you might delete. A deployed application
continues to use its own `.venv`; pydep invokes that environment's Python
directly and does not install FastAPI or Uvicorn for the application. Ensure the
CLI installation's Scripts directory is on PATH. If `pydep` is not found after
a pip installation, use `py -m pydep` with the same Python that installed it,
or add that Python installation's Scripts directory to PATH. No runtime
packages are required by pydep itself.

NSSM is a separate prerequisite: install it independently and make `nssm.exe`
available on PATH (or pass `--nssm` where supported). Open PowerShell **as
Administrator** for commands that change services: `deploy`, `start`, `stop`,
and `remove`. The read-only `list` and `logs` commands do not require elevation.

Use the same Windows account each time. Elevating with a different account uses
that account's `%APPDATA%` and therefore a different deployment registry.

## 3. Deploy D:\odin

For an existing application, keep its source and existing `.venv`. Ensure its Windows interpreter can import FastAPI and Uvicorn:

```powershell
Set-Location D:\odin
.\.venv\Scripts\python.exe -m pip install fastapi uvicorn
pydep deploy --alias odin
```

If you are creating a fresh example, first create the venv with `py -m venv .venv`, create `app\main.py`, and put this in it:

```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health():
    return {"status": "ok"}
```

Example output, assuming the first ID is available:

```text
[OK] Service reconciled: PyDep_A111
Deployment ID:  A111
Alias:          odin
Module:         app.main:app
Host:           0.0.0.0
Port:           8000
Path:           D:\odin
Status:         Running (Windows Service state)
```

Check the actual HTTP endpoint separately:

```powershell
Invoke-RestMethod http://127.0.0.1:8000/health
```

`Running` is the observed Windows Service state, not an HTTP health guarantee. pydep waits for three seconds of running state after a start. Application startup failures, slow lifespan hooks, or later failures still require checking application logs and your health endpoint.

## Commands

| Command | Behavior |
| --- | --- |
| `pydep deploy` | Detect and deploy the current directory, or reconcile its existing registration |
| `pydep deploy --alias odin` | Set or explicitly rename the alias |
| `pydep deploy --module app.main:app` | Explicit import target, also stored for later deploys |
| `pydep deploy --host 127.0.0.1 --port 8001` | Set listening address and port |
| `pydep deploy --restart` | Restart even if settings already match, to reload source changes |
| `pydep list` | Show actual SCM state and registry operation phase |
| `pydep start A111` or `pydep start odin` | Start the registered service |
| `pydep stop A111` or `pydep stop odin` | Stop the registered service |
| `pydep logs A111` or `pydep logs odin` | Show the last 50 lines of each log |
| `pydep logs odin --follow --lines 100` | Tail both streams; Ctrl+C exits |
| `pydep remove A111` or `pydep remove odin` | Remove service and registration, archive logs |
| `pydep remove odin --purge-logs` | Also delete this deployment's current and rotated logs |

Representative command output:

```text
> pydep list
ID     ALIAS                    STATUS       PORT   PHASE     PATH
A111   odin                     Running      8000   ready     D:\odin
A112   rental-api               Stopped      8001   ready     D:\rental-api

> pydep start odin
[OK] Started A111 (odin)

> pydep stop A111
[OK] Stopped A111 (odin)

> pydep logs odin
--- stdout: C:\Users\you\AppData\Roaming\pydep\logs\A111.out.log ---
INFO:     127.0.0.1:54321 - "GET /health HTTP/1.1" 200 OK
--- stderr: C:\Users\you\AppData\Roaming\pydep\logs\A111.err.log ---
INFO:     Application startup complete.

> pydep logs A111 --follow
--- stdout: C:\Users\you\AppData\Roaming\pydep\logs\A111.out.log ---
(no logs yet)
--- stderr: C:\Users\you\AppData\Roaming\pydep\logs\A111.err.log ---
INFO:     Application startup complete.
[stdout] INFO:     127.0.0.1:54322 - "GET /health HTTP/1.1" 200 OK

> pydep remove odin
[OK] Removed A111 (odin)

> pydep list
No deployments registered.
```

Outputs are illustrative. IDs depend on existing registrations and machine services. Commands exit with 0 on success, 1 on operational failure, 2 for invalid command syntax, and 130 on Ctrl+C.

## Identity and idempotency

The canonical, resolved, case-insensitive project path is the deployment key. Repeated deployment from the same directory preserves its ID. Omitted alias/module/host/port options preserve stored values. Explicit options change desired configuration. Source changes alone are not fingerprinted: use `--restart` to reload changed application code or installed dependencies.

NSSM settings are compared with desired settings on each deployment. Drift is repaired. A configuration change restarts the service; an unchanged running service is left alone. A stopped or missing service is started or recreated. An alias-only change does not restart Python.

IDs start at `A111`, continue through `A999`, then `B000` through `F999`. This makes the requested starting example explicit. The first free ID in that sequence is used, skipping machine services with occupied names. Removed IDs can be reused. Registry aliases are unique case-insensitively; ASCII letters, digits, and hyphens are allowed, up to 63 characters. Spaces and punctuation normalize to hyphens. ID-shaped aliases are rejected to prevent ambiguous resolution. Automatically generated alias collisions receive `-2`, `-3`, etc.; explicit collisions fail.

The registry is per Windows account; services are machine-wide. Aliases are unique within that account's registry. Another account's service name is skipped. Concurrent commands against one registry are serialized with a Windows file lock. Cross-account install races fail safely instead of taking over another service; they may require the recovery procedure below. Do not share a roaming registry across multiple machines or manage the same project under multiple accounts.

New deployments default to port 8000. There is no automatic port allocation. A port registered to another deployment in this account is rejected even if its service is stopped. Unregistered programs and other accounts can also occupy ports; inspect Uvicorn bind errors and choose `--port 8001` when necessary.

## FastAPI detection and execution

Discovery parses source with Python's AST; it does not import application modules. It supports `app/main.py`, `main.py`, and other `main.py` files up to four directory levels deep. It ignores `.venv`, `venv`, `.git`, `__pycache__`, hidden directories, tests, build output, node_modules, and directory symlinks.

Top-level instances created via `from fastapi import FastAPI`, renamed imports, or `import fastapi as f` are recognized. The instance can be named `app`, `api`, or another identifier. Ambiguous matches fail with the candidates rather than silently selecting the wrong app. Factories, dynamic creation, and src-layout import-path configuration are outside automatic detection. Use `--module package.module:instance` for an importable explicit target; factories are not supported.

The preflight imports FastAPI and Uvicorn using the project's Python. It does not import the selected app or verify its startup hooks. Uvicorn imports the target when the service runs.

The service uses the project's interpreter directly, with its working directory set to the project:

```text
D:\odin\.venv\Scripts\python.exe -u -m uvicorn app.main:app --host 0.0.0.0 --port 8000
```

Venv activation is unnecessary. One Uvicorn process is used, without development reload. All subprocess calls use argument arrays and `shell=False`. NSSM's stored `AppParameters` value uses Windows argv quoting, not shell interpolation.

## Storage and logs

pydep stores its own files under `%APPDATA%\pydep`:

- `deployments.json`: versioned schema, ID, alias, absolute project and interpreter paths, service name, module, host, port, UTC timestamps, and operation phase.
- `deployments.lock`: lock file; its presence does not mean a command is still running.
- `logs\A111.out.log` and `logs\A111.err.log`: persistent stdout/stderr.
- `logs\archive\A111-<timestamp>\...`: retained logs after removal, avoiding confusion when an ID is reused.

No application secrets or environment values are captured in the registry. Application logs can still contain sensitive information emitted by your code. Rotation is configured at approximately 10 MiB per active stream with NSSM online rotation. Rotated files are retained; there is no retention-age or total-disk-size cap. Apply your own retention policy. `logs` reads the current files and follows truncation or file replacement; recent output is limited to the last 1 MiB per stream. It does not merge streams chronologically or read historical archives.

`remove` stops and deletes only the owned Windows Service, then archives or purges deployment log files and removes its registry record. It never recursively removes a project, source directory, or `.venv`. Empty shared metadata directories and the lock file remain. Previously archived logs are not affected by `--purge-logs` for a newly reused ID.

## Windows persistence and service identity

Windows Service Control Manager owns the NSSM service lifecycle. pydep configures automatic startup and NSSM application restart behavior, so closing PowerShell does not stop the API. Windows starts the service after reboot; NSSM supervises the Python process. See the [NSSM command reference](https://nssm.cc/commands).

New services use NSSM's default **LocalSystem** account, which is highly privileged. Deploy only trusted applications, dependencies, and virtual environments. Protect the project, Python installation, NSSM executable, and metadata/log directories against modification by untrusted users. This small tool is intended for administrator-controlled hosts, not mutually untrusted tenants.

For a hardened host, after first deployment stop the service and select an appropriate dedicated account in `services.msc` under **Log On**. Grant that account read/execute access to the project, venv, base Python installation, and NSSM, plus write access to the log directory and any application data directories. Configure credentials through Windows, not pydep. Redeploy preserves an existing service's account. If a service was deleted and recreated, its account returns to NSSM's default and must be configured again.

Services do not inherit your interactive PowerShell environment, mapped network drives, or user login state. Use a local, continuously available project and interpreter. Manage application secrets through your existing service-compatible mechanism. pydep does not add firewall rules, TLS, a reverse proxy, or readiness dependencies. The default host binds all interfaces; choose `127.0.0.1` when placing IIS or another local proxy in front.

## Failure recovery

Registry writes use a same-directory temporary file, flush/fsync, and atomic replacement. A desired-state record is saved **before** service mutation. SCM and JSON cannot be committed as one transaction, so failed or interrupted operations retain a visible phase:

- `pending`: deployment/configuration/start was not fully completed. Fix the cause, return to the project, and rerun `pydep deploy`. The ID and desired alias/settings are retained. There is no automatic rollback to the old working configuration.
- `removing`: removal was interrupted. Rerun `pydep remove <id>`. Do not redeploy until removal finishes.
- `ready`: last deployment completed; current service status is queried separately.

The service description contains a pydep ownership marker derived from the metadata directory and deployment ID. Keep it unchanged. An ownership mismatch causes management to fail safely. This is an accidental-takeover guard, not a security boundary against administrators.

If execution stops in the small window between NSSM install and setting the description, the service may exist without a marker. Do not blindly remove it. First inspect `services.msc`, the pending JSON record, and:

```powershell
nssm get PyDep_A111 Application
nssm get PyDep_A111 AppDirectory
nssm get PyDep_A111 Description
```

Only after confirming it is the service created for this deployment, stop it if running and remove it using `nssm remove PyDep_A111 confirm`. Keep the pending JSON record and rerun deploy. Never delete an unrelated service to free an ID.

For a corrupt registry, restore a known-good backup; pydep refuses to overwrite malformed content. Back up the registry before manual edits. If Windows reports a service is marked for deletion, close Services/Event Viewer handles and retry removal. If startup fails, inspect `pydep logs <id>` and Windows Event Viewer for missing imports, occupied ports, invalid settings, or account/path permissions.

## Manual Windows acceptance test

Use a test machine and a fresh example project; replace the sample ID with the ID actually returned.

1. Install NSSM and pydep, create the example under `D:\odin`, and deploy it. Verify `/health` returns `{"status":"ok"}`.
2. Run `pydep deploy` three more times. Confirm one registry row, one Windows Service, and the same ID/alias. Inspect `nssm get PyDep_A111 AppParameters` and `Start`.
3. Run `pydep deploy --alias renamed`; confirm the ID is unchanged, `start renamed` works, and the old alias is unknown. Rename back to `odin`.
4. Stop by ID and confirm HTTP is unavailable; start by alias and confirm HTTP recovers. Repeat start/stop in the already requested state.
5. Make HTTP requests and inspect `logs odin`, then `logs A111 --follow`. Stop and start the service; confirm existing output remains. Trigger `nssm rotate PyDep_A111`, issue more requests, and confirm follow continues into the new files.
6. Close PowerShell. Check the endpoint from a new window. Reboot the test machine, then verify `/health` without running pydep start. Inspect the automatic startup setting and service account.
7. Change service AppParameters manually to port 8999. Run `pydep deploy`; confirm the stored desired port is restored and the service restarts. Edit application code and run `pydep deploy --restart` to verify the new response.
8. Create a second project. Confirm an explicit duplicate alias fails; deploy it with another alias and `--port 8001`. Confirm both appear once in list.
9. Test missing venv/Python, `--port 0`, unknown ID/alias, and ambiguous FastAPI instances. Run a mutating command from a non-elevated terminal and verify the Administrator error. Test missing NSSM with an invalid `--nssm` path.
10. In the disposable project, set an invalid import target with `--module missing:app`. Verify deployment fails or fails its HTTP check; inspect status/logs. Redeploy with the correct module and verify the same ID is recovered.
11. Run `pydep remove odin`. Confirm `Get-Service PyDep_A111 -ErrorAction SilentlyContinue` returns nothing, list loses only that record, logs are archived, and all source and venv files still exist. Remove the second test deployment too.

## Automated tests and layout

From the extracted package directory:

```powershell
py -m pip install .
py -m unittest discover -s tests -v
```

The suite tests behavior with a service double and mocks the NSSM process boundary. It does not install or delete real services. The manual procedure above covers integration.

| File | Purpose |
| --- | --- |
| `pyproject.toml` | Installable package and global console entry point |
| `src/pydep/cli.py` | Commands, validation at CLI boundary, log display |
| `src/pydep/core.py` | Discovery, identity, locked registry, reconciliation/removal |
| `src/pydep/windows.py` | SCM queries, NSSM configuration and lifecycle |
| `src/pydep/__init__.py` | Package version |
| `src/pydep/__main__.py` | `python -m pydep` support |
| `tests/test_pydep.py` | Automated behavior tests |

Settings are centralized in `NSSM.configure`; registry schema version 1 keeps future extension explicit. No database, cloud, Docker, or large CLI framework is used.
