pydep-service 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zubair Jamil
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,299 @@
1
+ Metadata-Version: 2.4
2
+ Name: pydep-service
3
+ Version: 0.1.0
4
+ Summary: Deploy FastAPI applications as persistent NSSM Windows Services
5
+ Author: Zubair Jamil
6
+ License-Expression: MIT
7
+ Keywords: deployment,fastapi,nssm,service,system-administration,uvicorn,windows
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: System :: Installation/Setup
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # pydep
27
+
28
+ 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.
29
+
30
+ This package includes implementation and automated behavior tests. The tests use
31
+ a service double, so actual SCM/NSSM integration, account permissions, and
32
+ reboot behavior must still be verified on Windows using the procedure below.
33
+ Passing the automated suite is not a claim that those integration checks ran.
34
+
35
+ ## 1. Install NSSM
36
+
37
+ 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).
38
+ 2. Extract the appropriate architecture's `nssm.exe` into a permanent, administrator-controlled location, for example `C:\Tools\nssm\nssm.exe`.
39
+ 3. Add `C:\Tools\nssm` to your Windows PATH through **System Properties > Environment Variables**, then open a new PowerShell window.
40
+ 4. Verify discovery:
41
+
42
+ ```powershell
43
+ Get-Command nssm.exe
44
+ nssm.exe version
45
+ ```
46
+
47
+ 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.
48
+
49
+ 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).
50
+
51
+ ## 2. Install pydep
52
+
53
+ The package published on PyPI is named `pydep-service`, while the installed
54
+ command remains `pydep`. The recommended installation uses uv's isolated tool
55
+ environment:
56
+
57
+ ```powershell
58
+ uv tool install pydep-service
59
+ pydep --version
60
+ pydep --help
61
+ ```
62
+
63
+ Run it once without installing it permanently:
64
+
65
+ ```powershell
66
+ uvx --from pydep-service pydep --help
67
+ ```
68
+
69
+ Alternative installation methods are:
70
+
71
+ ```powershell
72
+ pipx install pydep-service
73
+ py -m pip install pydep-service
74
+ ```
75
+
76
+ Install the CLI into a stable, isolated tool environment or Python
77
+ installation, not an application venv you might delete. A deployed application
78
+ continues to use its own `.venv`; pydep invokes that environment's Python
79
+ directly and does not install FastAPI or Uvicorn for the application. Ensure the
80
+ CLI installation's Scripts directory is on PATH. If `pydep` is not found after
81
+ a pip installation, use `py -m pydep` with the same Python that installed it,
82
+ or add that Python installation's Scripts directory to PATH. No runtime
83
+ packages are required by pydep itself.
84
+
85
+ NSSM is a separate prerequisite: install it independently and make `nssm.exe`
86
+ available on PATH (or pass `--nssm` where supported). Open PowerShell **as
87
+ Administrator** for commands that change services: `deploy`, `start`, `stop`,
88
+ and `remove`. The read-only `list` and `logs` commands do not require elevation.
89
+
90
+ Use the same Windows account each time. Elevating with a different account uses
91
+ that account's `%APPDATA%` and therefore a different deployment registry.
92
+
93
+ ## 3. Deploy D:\odin
94
+
95
+ For an existing application, keep its source and existing `.venv`. Ensure its Windows interpreter can import FastAPI and Uvicorn:
96
+
97
+ ```powershell
98
+ Set-Location D:\odin
99
+ .\.venv\Scripts\python.exe -m pip install fastapi uvicorn
100
+ pydep deploy --alias odin
101
+ ```
102
+
103
+ 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:
104
+
105
+ ```python
106
+ from fastapi import FastAPI
107
+
108
+ app = FastAPI()
109
+
110
+ @app.get("/health")
111
+ def health():
112
+ return {"status": "ok"}
113
+ ```
114
+
115
+ Example output, assuming the first ID is available:
116
+
117
+ ```text
118
+ [OK] Service reconciled: PyDep_A111
119
+ Deployment ID: A111
120
+ Alias: odin
121
+ Module: app.main:app
122
+ Host: 0.0.0.0
123
+ Port: 8000
124
+ Path: D:\odin
125
+ Status: Running (Windows Service state)
126
+ ```
127
+
128
+ Check the actual HTTP endpoint separately:
129
+
130
+ ```powershell
131
+ Invoke-RestMethod http://127.0.0.1:8000/health
132
+ ```
133
+
134
+ `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.
135
+
136
+ ## Commands
137
+
138
+ | Command | Behavior |
139
+ | --- | --- |
140
+ | `pydep deploy` | Detect and deploy the current directory, or reconcile its existing registration |
141
+ | `pydep deploy --alias odin` | Set or explicitly rename the alias |
142
+ | `pydep deploy --module app.main:app` | Explicit import target, also stored for later deploys |
143
+ | `pydep deploy --host 127.0.0.1 --port 8001` | Set listening address and port |
144
+ | `pydep deploy --restart` | Restart even if settings already match, to reload source changes |
145
+ | `pydep list` | Show actual SCM state and registry operation phase |
146
+ | `pydep start A111` or `pydep start odin` | Start the registered service |
147
+ | `pydep stop A111` or `pydep stop odin` | Stop the registered service |
148
+ | `pydep logs A111` or `pydep logs odin` | Show the last 50 lines of each log |
149
+ | `pydep logs odin --follow --lines 100` | Tail both streams; Ctrl+C exits |
150
+ | `pydep remove A111` or `pydep remove odin` | Remove service and registration, archive logs |
151
+ | `pydep remove odin --purge-logs` | Also delete this deployment's current and rotated logs |
152
+
153
+ Representative command output:
154
+
155
+ ```text
156
+ > pydep list
157
+ ID ALIAS STATUS PORT PHASE PATH
158
+ A111 odin Running 8000 ready D:\odin
159
+ A112 rental-api Stopped 8001 ready D:\rental-api
160
+
161
+ > pydep start odin
162
+ [OK] Started A111 (odin)
163
+
164
+ > pydep stop A111
165
+ [OK] Stopped A111 (odin)
166
+
167
+ > pydep logs odin
168
+ --- stdout: C:\Users\you\AppData\Roaming\pydep\logs\A111.out.log ---
169
+ INFO: 127.0.0.1:54321 - "GET /health HTTP/1.1" 200 OK
170
+ --- stderr: C:\Users\you\AppData\Roaming\pydep\logs\A111.err.log ---
171
+ INFO: Application startup complete.
172
+
173
+ > pydep logs A111 --follow
174
+ --- stdout: C:\Users\you\AppData\Roaming\pydep\logs\A111.out.log ---
175
+ (no logs yet)
176
+ --- stderr: C:\Users\you\AppData\Roaming\pydep\logs\A111.err.log ---
177
+ INFO: Application startup complete.
178
+ [stdout] INFO: 127.0.0.1:54322 - "GET /health HTTP/1.1" 200 OK
179
+
180
+ > pydep remove odin
181
+ [OK] Removed A111 (odin)
182
+
183
+ > pydep list
184
+ No deployments registered.
185
+ ```
186
+
187
+ 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.
188
+
189
+ ## Identity and idempotency
190
+
191
+ 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.
192
+
193
+ 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.
194
+
195
+ 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.
196
+
197
+ 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.
198
+
199
+ 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.
200
+
201
+ ## FastAPI detection and execution
202
+
203
+ 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.
204
+
205
+ 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.
206
+
207
+ 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.
208
+
209
+ The service uses the project's interpreter directly, with its working directory set to the project:
210
+
211
+ ```text
212
+ D:\odin\.venv\Scripts\python.exe -u -m uvicorn app.main:app --host 0.0.0.0 --port 8000
213
+ ```
214
+
215
+ 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.
216
+
217
+ ## Storage and logs
218
+
219
+ pydep stores its own files under `%APPDATA%\pydep`:
220
+
221
+ - `deployments.json`: versioned schema, ID, alias, absolute project and interpreter paths, service name, module, host, port, UTC timestamps, and operation phase.
222
+ - `deployments.lock`: lock file; its presence does not mean a command is still running.
223
+ - `logs\A111.out.log` and `logs\A111.err.log`: persistent stdout/stderr.
224
+ - `logs\archive\A111-<timestamp>\...`: retained logs after removal, avoiding confusion when an ID is reused.
225
+
226
+ 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.
227
+
228
+ `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.
229
+
230
+ ## Windows persistence and service identity
231
+
232
+ 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).
233
+
234
+ 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.
235
+
236
+ 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.
237
+
238
+ 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.
239
+
240
+ ## Failure recovery
241
+
242
+ 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:
243
+
244
+ - `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.
245
+ - `removing`: removal was interrupted. Rerun `pydep remove <id>`. Do not redeploy until removal finishes.
246
+ - `ready`: last deployment completed; current service status is queried separately.
247
+
248
+ 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.
249
+
250
+ 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:
251
+
252
+ ```powershell
253
+ nssm get PyDep_A111 Application
254
+ nssm get PyDep_A111 AppDirectory
255
+ nssm get PyDep_A111 Description
256
+ ```
257
+
258
+ 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.
259
+
260
+ 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.
261
+
262
+ ## Manual Windows acceptance test
263
+
264
+ Use a test machine and a fresh example project; replace the sample ID with the ID actually returned.
265
+
266
+ 1. Install NSSM and pydep, create the example under `D:\odin`, and deploy it. Verify `/health` returns `{"status":"ok"}`.
267
+ 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`.
268
+ 3. Run `pydep deploy --alias renamed`; confirm the ID is unchanged, `start renamed` works, and the old alias is unknown. Rename back to `odin`.
269
+ 4. Stop by ID and confirm HTTP is unavailable; start by alias and confirm HTTP recovers. Repeat start/stop in the already requested state.
270
+ 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.
271
+ 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.
272
+ 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.
273
+ 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.
274
+ 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.
275
+ 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.
276
+ 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.
277
+
278
+ ## Automated tests and layout
279
+
280
+ From the extracted package directory:
281
+
282
+ ```powershell
283
+ py -m pip install .
284
+ py -m unittest discover -s tests -v
285
+ ```
286
+
287
+ 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.
288
+
289
+ | File | Purpose |
290
+ | --- | --- |
291
+ | `pyproject.toml` | Installable package and global console entry point |
292
+ | `src/pydep/cli.py` | Commands, validation at CLI boundary, log display |
293
+ | `src/pydep/core.py` | Discovery, identity, locked registry, reconciliation/removal |
294
+ | `src/pydep/windows.py` | SCM queries, NSSM configuration and lifecycle |
295
+ | `src/pydep/__init__.py` | Package version |
296
+ | `src/pydep/__main__.py` | `python -m pydep` support |
297
+ | `tests/test_pydep.py` | Automated behavior tests |
298
+
299
+ Settings are centralized in `NSSM.configure`; registry schema version 1 keeps future extension explicit. No database, cloud, Docker, or large CLI framework is used.
@@ -0,0 +1,274 @@
1
+ # pydep
2
+
3
+ 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.
4
+
5
+ This package includes implementation and automated behavior tests. The tests use
6
+ a service double, so actual SCM/NSSM integration, account permissions, and
7
+ reboot behavior must still be verified on Windows using the procedure below.
8
+ Passing the automated suite is not a claim that those integration checks ran.
9
+
10
+ ## 1. Install NSSM
11
+
12
+ 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).
13
+ 2. Extract the appropriate architecture's `nssm.exe` into a permanent, administrator-controlled location, for example `C:\Tools\nssm\nssm.exe`.
14
+ 3. Add `C:\Tools\nssm` to your Windows PATH through **System Properties > Environment Variables**, then open a new PowerShell window.
15
+ 4. Verify discovery:
16
+
17
+ ```powershell
18
+ Get-Command nssm.exe
19
+ nssm.exe version
20
+ ```
21
+
22
+ 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.
23
+
24
+ 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).
25
+
26
+ ## 2. Install pydep
27
+
28
+ The package published on PyPI is named `pydep-service`, while the installed
29
+ command remains `pydep`. The recommended installation uses uv's isolated tool
30
+ environment:
31
+
32
+ ```powershell
33
+ uv tool install pydep-service
34
+ pydep --version
35
+ pydep --help
36
+ ```
37
+
38
+ Run it once without installing it permanently:
39
+
40
+ ```powershell
41
+ uvx --from pydep-service pydep --help
42
+ ```
43
+
44
+ Alternative installation methods are:
45
+
46
+ ```powershell
47
+ pipx install pydep-service
48
+ py -m pip install pydep-service
49
+ ```
50
+
51
+ Install the CLI into a stable, isolated tool environment or Python
52
+ installation, not an application venv you might delete. A deployed application
53
+ continues to use its own `.venv`; pydep invokes that environment's Python
54
+ directly and does not install FastAPI or Uvicorn for the application. Ensure the
55
+ CLI installation's Scripts directory is on PATH. If `pydep` is not found after
56
+ a pip installation, use `py -m pydep` with the same Python that installed it,
57
+ or add that Python installation's Scripts directory to PATH. No runtime
58
+ packages are required by pydep itself.
59
+
60
+ NSSM is a separate prerequisite: install it independently and make `nssm.exe`
61
+ available on PATH (or pass `--nssm` where supported). Open PowerShell **as
62
+ Administrator** for commands that change services: `deploy`, `start`, `stop`,
63
+ and `remove`. The read-only `list` and `logs` commands do not require elevation.
64
+
65
+ Use the same Windows account each time. Elevating with a different account uses
66
+ that account's `%APPDATA%` and therefore a different deployment registry.
67
+
68
+ ## 3. Deploy D:\odin
69
+
70
+ For an existing application, keep its source and existing `.venv`. Ensure its Windows interpreter can import FastAPI and Uvicorn:
71
+
72
+ ```powershell
73
+ Set-Location D:\odin
74
+ .\.venv\Scripts\python.exe -m pip install fastapi uvicorn
75
+ pydep deploy --alias odin
76
+ ```
77
+
78
+ 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:
79
+
80
+ ```python
81
+ from fastapi import FastAPI
82
+
83
+ app = FastAPI()
84
+
85
+ @app.get("/health")
86
+ def health():
87
+ return {"status": "ok"}
88
+ ```
89
+
90
+ Example output, assuming the first ID is available:
91
+
92
+ ```text
93
+ [OK] Service reconciled: PyDep_A111
94
+ Deployment ID: A111
95
+ Alias: odin
96
+ Module: app.main:app
97
+ Host: 0.0.0.0
98
+ Port: 8000
99
+ Path: D:\odin
100
+ Status: Running (Windows Service state)
101
+ ```
102
+
103
+ Check the actual HTTP endpoint separately:
104
+
105
+ ```powershell
106
+ Invoke-RestMethod http://127.0.0.1:8000/health
107
+ ```
108
+
109
+ `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.
110
+
111
+ ## Commands
112
+
113
+ | Command | Behavior |
114
+ | --- | --- |
115
+ | `pydep deploy` | Detect and deploy the current directory, or reconcile its existing registration |
116
+ | `pydep deploy --alias odin` | Set or explicitly rename the alias |
117
+ | `pydep deploy --module app.main:app` | Explicit import target, also stored for later deploys |
118
+ | `pydep deploy --host 127.0.0.1 --port 8001` | Set listening address and port |
119
+ | `pydep deploy --restart` | Restart even if settings already match, to reload source changes |
120
+ | `pydep list` | Show actual SCM state and registry operation phase |
121
+ | `pydep start A111` or `pydep start odin` | Start the registered service |
122
+ | `pydep stop A111` or `pydep stop odin` | Stop the registered service |
123
+ | `pydep logs A111` or `pydep logs odin` | Show the last 50 lines of each log |
124
+ | `pydep logs odin --follow --lines 100` | Tail both streams; Ctrl+C exits |
125
+ | `pydep remove A111` or `pydep remove odin` | Remove service and registration, archive logs |
126
+ | `pydep remove odin --purge-logs` | Also delete this deployment's current and rotated logs |
127
+
128
+ Representative command output:
129
+
130
+ ```text
131
+ > pydep list
132
+ ID ALIAS STATUS PORT PHASE PATH
133
+ A111 odin Running 8000 ready D:\odin
134
+ A112 rental-api Stopped 8001 ready D:\rental-api
135
+
136
+ > pydep start odin
137
+ [OK] Started A111 (odin)
138
+
139
+ > pydep stop A111
140
+ [OK] Stopped A111 (odin)
141
+
142
+ > pydep logs odin
143
+ --- stdout: C:\Users\you\AppData\Roaming\pydep\logs\A111.out.log ---
144
+ INFO: 127.0.0.1:54321 - "GET /health HTTP/1.1" 200 OK
145
+ --- stderr: C:\Users\you\AppData\Roaming\pydep\logs\A111.err.log ---
146
+ INFO: Application startup complete.
147
+
148
+ > pydep logs A111 --follow
149
+ --- stdout: C:\Users\you\AppData\Roaming\pydep\logs\A111.out.log ---
150
+ (no logs yet)
151
+ --- stderr: C:\Users\you\AppData\Roaming\pydep\logs\A111.err.log ---
152
+ INFO: Application startup complete.
153
+ [stdout] INFO: 127.0.0.1:54322 - "GET /health HTTP/1.1" 200 OK
154
+
155
+ > pydep remove odin
156
+ [OK] Removed A111 (odin)
157
+
158
+ > pydep list
159
+ No deployments registered.
160
+ ```
161
+
162
+ 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.
163
+
164
+ ## Identity and idempotency
165
+
166
+ 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.
167
+
168
+ 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.
169
+
170
+ 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.
171
+
172
+ 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.
173
+
174
+ 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.
175
+
176
+ ## FastAPI detection and execution
177
+
178
+ 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.
179
+
180
+ 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.
181
+
182
+ 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.
183
+
184
+ The service uses the project's interpreter directly, with its working directory set to the project:
185
+
186
+ ```text
187
+ D:\odin\.venv\Scripts\python.exe -u -m uvicorn app.main:app --host 0.0.0.0 --port 8000
188
+ ```
189
+
190
+ 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.
191
+
192
+ ## Storage and logs
193
+
194
+ pydep stores its own files under `%APPDATA%\pydep`:
195
+
196
+ - `deployments.json`: versioned schema, ID, alias, absolute project and interpreter paths, service name, module, host, port, UTC timestamps, and operation phase.
197
+ - `deployments.lock`: lock file; its presence does not mean a command is still running.
198
+ - `logs\A111.out.log` and `logs\A111.err.log`: persistent stdout/stderr.
199
+ - `logs\archive\A111-<timestamp>\...`: retained logs after removal, avoiding confusion when an ID is reused.
200
+
201
+ 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.
202
+
203
+ `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.
204
+
205
+ ## Windows persistence and service identity
206
+
207
+ 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).
208
+
209
+ 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.
210
+
211
+ 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.
212
+
213
+ 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.
214
+
215
+ ## Failure recovery
216
+
217
+ 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:
218
+
219
+ - `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.
220
+ - `removing`: removal was interrupted. Rerun `pydep remove <id>`. Do not redeploy until removal finishes.
221
+ - `ready`: last deployment completed; current service status is queried separately.
222
+
223
+ 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.
224
+
225
+ 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:
226
+
227
+ ```powershell
228
+ nssm get PyDep_A111 Application
229
+ nssm get PyDep_A111 AppDirectory
230
+ nssm get PyDep_A111 Description
231
+ ```
232
+
233
+ 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.
234
+
235
+ 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.
236
+
237
+ ## Manual Windows acceptance test
238
+
239
+ Use a test machine and a fresh example project; replace the sample ID with the ID actually returned.
240
+
241
+ 1. Install NSSM and pydep, create the example under `D:\odin`, and deploy it. Verify `/health` returns `{"status":"ok"}`.
242
+ 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`.
243
+ 3. Run `pydep deploy --alias renamed`; confirm the ID is unchanged, `start renamed` works, and the old alias is unknown. Rename back to `odin`.
244
+ 4. Stop by ID and confirm HTTP is unavailable; start by alias and confirm HTTP recovers. Repeat start/stop in the already requested state.
245
+ 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.
246
+ 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.
247
+ 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.
248
+ 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.
249
+ 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.
250
+ 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.
251
+ 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.
252
+
253
+ ## Automated tests and layout
254
+
255
+ From the extracted package directory:
256
+
257
+ ```powershell
258
+ py -m pip install .
259
+ py -m unittest discover -s tests -v
260
+ ```
261
+
262
+ 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.
263
+
264
+ | File | Purpose |
265
+ | --- | --- |
266
+ | `pyproject.toml` | Installable package and global console entry point |
267
+ | `src/pydep/cli.py` | Commands, validation at CLI boundary, log display |
268
+ | `src/pydep/core.py` | Discovery, identity, locked registry, reconciliation/removal |
269
+ | `src/pydep/windows.py` | SCM queries, NSSM configuration and lifecycle |
270
+ | `src/pydep/__init__.py` | Package version |
271
+ | `src/pydep/__main__.py` | `python -m pydep` support |
272
+ | `tests/test_pydep.py` | Automated behavior tests |
273
+
274
+ Settings are centralized in `NSSM.configure`; registry schema version 1 keeps future extension explicit. No database, cloud, Docker, or large CLI framework is used.
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pydep-service"
7
+ version = "0.1.0"
8
+ description = "Deploy FastAPI applications as persistent NSSM Windows Services"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Zubair Jamil" },
14
+ ]
15
+ dependencies = []
16
+ keywords = [
17
+ "deployment",
18
+ "fastapi",
19
+ "nssm",
20
+ "service",
21
+ "system-administration",
22
+ "uvicorn",
23
+ "windows",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Environment :: Console",
28
+ "Intended Audience :: Developers",
29
+ "Intended Audience :: System Administrators",
30
+ "Operating System :: Microsoft :: Windows",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3 :: Only",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Topic :: System :: Installation/Setup",
38
+ "Topic :: System :: Systems Administration",
39
+ ]
40
+
41
+ [project.scripts]
42
+ pydep = "pydep.cli:main"
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """Small Windows service deployment CLI."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())