iocmng 2.0.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.
iocmng-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,378 @@
1
+ Metadata-Version: 2.4
2
+ Name: iocmng
3
+ Version: 2.0.0
4
+ Summary: A pluggable task/job framework for IOC Manager applications with REST API
5
+ Author: INFN Beamline Controls
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/infn-epics/epik8s-beamline-controller
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: Physics
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: fastapi>=0.100.0
20
+ Requires-Dist: uvicorn[standard]>=0.20.0
21
+ Requires-Dist: pyyaml>=6.0
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: softioc
24
+ Requires-Dist: cothread
25
+ Provides-Extra: ophyd
26
+ Requires-Dist: ophyd; extra == "ophyd"
27
+ Requires-Dist: infn_ophyd_hal; extra == "ophyd"
28
+ Provides-Extra: kubernetes
29
+ Requires-Dist: kubernetes; extra == "kubernetes"
30
+ Provides-Extra: all
31
+ Requires-Dist: iocmng[kubernetes,ophyd]; extra == "all"
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0; extra == "dev"
34
+ Requires-Dist: pytest-asyncio; extra == "dev"
35
+ Requires-Dist: httpx; extra == "dev"
36
+ Requires-Dist: black; extra == "dev"
37
+ Requires-Dist: flake8; extra == "dev"
38
+ Requires-Dist: build; extra == "dev"
39
+ Requires-Dist: twine; extra == "dev"
40
+
41
+ # iocmng — IOC Manager Framework
42
+
43
+ A pluggable task/job framework for IOC Manager applications. Provides base classes for continuous **tasks** and one-shot **jobs** that can be dynamically loaded at runtime via a REST API.
44
+
45
+ ## Features
46
+
47
+ - **`TaskBase`** — base class for continuous tasks (run in a loop)
48
+ - **`JobBase`** — base class for one-shot jobs (run once, return result)
49
+ - **REST API** — add/remove tasks and jobs at runtime from git repositories
50
+ - **Validation** — plugins are validated (must derive from base class, must compile, abstract methods must be implemented)
51
+ - **EPICS soft IOC PVs** — every task and job gets default PVs (STATUS, MESSAGE, etc.) via `softioc`
52
+ - **Per-plugin `config.yaml`** — each plugin defines its PVs and parameters in a config file inside its git repo
53
+ - **Path support** — specify a sub-directory inside the git repo where the plugin sources live
54
+ - **Plugin `requirements.txt`** — plugins can ship their own dependencies
55
+ - **Optional Ophyd integration** — device abstraction via `ophyd`/`infn_ophyd_hal` (optional dependency)
56
+ - **Docker image** — ready-to-run container with the REST API
57
+ - **PyPI package** — `pip install iocmng`
58
+
59
+ ## Quick Start
60
+
61
+ ### Install from PyPI
62
+
63
+ ```bash
64
+ pip install iocmng
65
+
66
+ # With all optional dependencies (ophyd, kubernetes)
67
+ pip install iocmng[all]
68
+ ```
69
+
70
+ ### Run the API Server
71
+
72
+ ```bash
73
+ # Using the CLI entry point
74
+ iocmng-server
75
+
76
+ # Or with environment variables
77
+ IOCMNG_PORT=8080 IOCMNG_LOG_LEVEL=debug iocmng-server
78
+
79
+ # Or with Docker
80
+ docker run -p 8080:8080 ghcr.io/infn-epics/epik8s-beamline-controller:latest
81
+ ```
82
+
83
+ ### Create a Task
84
+
85
+ Create a git repository with:
86
+ 1. A Python file with a class deriving from `TaskBase`
87
+ 2. A `config.yaml` defining PVs and parameters
88
+
89
+ ```
90
+ my-monitor-repo/
91
+ ├── my_monitor.py
92
+ ├── config.yaml
93
+ └── requirements.txt # optional — extra dependencies
94
+ ```
95
+
96
+ **my_monitor.py**
97
+ ```python
98
+ from iocmng import TaskBase
99
+
100
+ class MyMonitor(TaskBase):
101
+ def initialize(self):
102
+ self.logger.info("Starting monitor")
103
+
104
+ def execute(self):
105
+ value = self.read_sensor()
106
+ self.set_pv("READING", value)
107
+ if value > self.parameters.get("threshold", 75):
108
+ self.set_pv("ALARM", 1)
109
+
110
+ def cleanup(self):
111
+ self.logger.info("Stopping monitor")
112
+
113
+ def read_sensor(self):
114
+ return 42.0
115
+ ```
116
+
117
+ **config.yaml**
118
+ ```yaml
119
+ parameters:
120
+ mode: continuous
121
+ interval: 1.0
122
+ threshold: 75.0
123
+
124
+ pvs:
125
+ inputs:
126
+ SETPOINT:
127
+ type: float
128
+ value: 50.0
129
+ unit: "%"
130
+ prec: 2
131
+ low: 0
132
+ high: 100
133
+ outputs:
134
+ READING:
135
+ type: float
136
+ value: 0.0
137
+ unit: "arb"
138
+ prec: 3
139
+ ALARM:
140
+ type: bool
141
+ value: 0
142
+ znam: "OK"
143
+ onam: "ALARM"
144
+ ```
145
+
146
+ ### Create a Job
147
+
148
+ ```python
149
+ # my_diagnostics.py
150
+ from iocmng import JobBase
151
+ from iocmng.base.job import JobResult
152
+
153
+ class MyDiagnostics(JobBase):
154
+ def initialize(self):
155
+ self.logger.info("Preparing diagnostics")
156
+
157
+ def execute(self) -> JobResult:
158
+ info = {"status": "healthy", "uptime": 12345}
159
+ self.set_pv("SYSTEM_NAME", info["status"])
160
+ return JobResult(success=True, data=info, message="Diagnostics OK")
161
+ ```
162
+
163
+ ### REST API Usage
164
+
165
+ #### Add a task
166
+ ```bash
167
+ curl -X POST http://localhost:8080/api/v1/tasks \
168
+ -H "Content-Type: application/json" \
169
+ -d '{
170
+ "name": "my-monitor",
171
+ "git_url": "https://github.com/user/my-monitor-task.git",
172
+ "pat": "ghp_optional_token",
173
+ "branch": "main",
174
+ "path": "src/monitor",
175
+ "parameters": {"threshold": 80.0}
176
+ }'
177
+ ```
178
+
179
+ The `path` field specifies the sub-directory inside the repo where the Python file and `config.yaml` live. Parameters passed in the REST request override values from `config.yaml`.
180
+
181
+ #### Remove a task
182
+ ```bash
183
+ curl -X DELETE http://localhost:8080/api/v1/tasks/my-monitor
184
+ ```
185
+
186
+ #### Add a job
187
+ ```bash
188
+ curl -X POST http://localhost:8080/api/v1/jobs \
189
+ -H "Content-Type: application/json" \
190
+ -d '{
191
+ "name": "my-diag",
192
+ "git_url": "https://github.com/user/my-diagnostics-job.git",
193
+ "path": "jobs/diagnostics"
194
+ }'
195
+ ```
196
+
197
+ #### Run a job
198
+ ```bash
199
+ curl -X POST http://localhost:8080/api/v1/jobs/my-diag/run
200
+ ```
201
+
202
+ #### Remove a job
203
+ ```bash
204
+ curl -X DELETE http://localhost:8080/api/v1/jobs/my-diag
205
+ ```
206
+
207
+ #### List all tasks
208
+ ```bash
209
+ curl http://localhost:8080/api/v1/tasks
210
+ ```
211
+
212
+ #### Health check
213
+ ```bash
214
+ curl http://localhost:8080/api/v1/health
215
+ ```
216
+
217
+ ## Plugin Structure
218
+
219
+ Each plugin lives in a git repository (or a sub-directory of one). The expected layout:
220
+
221
+ ```
222
+ <repo-root>/
223
+ └── <path>/ # optional sub-directory (specified via REST "path" field)
224
+ ├── my_plugin.py # Python module with TaskBase/JobBase subclass
225
+ ├── config.yaml # Plugin configuration (PVs, parameters)
226
+ └── requirements.txt # Optional additional pip dependencies
227
+ ```
228
+
229
+ ### config.yaml Format
230
+
231
+ ```yaml
232
+ # Parameters — passed to the plugin constructor as self.parameters
233
+ # REST-supplied parameters override these defaults
234
+ parameters:
235
+ mode: continuous # "continuous" or "triggered"
236
+ interval: 1.0 # application-specific
237
+ threshold: 75.0 # application-specific
238
+
239
+ # PV definitions — created automatically by the IOC Manager
240
+ pvs:
241
+ inputs: # writable PVs (operator → plugin)
242
+ SETPOINT:
243
+ type: float # float, int, string, bool
244
+ value: 50.0 # initial value
245
+ unit: "%" # EGU (float only)
246
+ prec: 2 # precision (float only)
247
+ low: 0 # LOPR (float only)
248
+ high: 100 # HOPR (float only)
249
+ outputs: # read-only PVs (plugin → operator)
250
+ READING:
251
+ type: float
252
+ value: 0.0
253
+ ALARM:
254
+ type: bool
255
+ value: 0
256
+ znam: "OK" # zero-state name (bool only)
257
+ onam: "ALARM" # one-state name (bool only)
258
+ ```
259
+
260
+ ## Plugin Validation
261
+
262
+ When a task or job is added, the framework performs the following checks:
263
+
264
+ 1. **Clone** — the git repository is cloned (with optional PAT for private repos)
265
+ 2. **Dependencies** — `requirements.txt` is installed if present (from `path` or repo root)
266
+ 3. **Config** — `config.yaml` is loaded from `path` to read PV definitions and default parameters
267
+ 4. **Syntax** — Python files are parsed via AST for syntax errors
268
+ 5. **Import** — the module is imported to check for runtime import errors
269
+ 6. **Inheritance** — at least one class must derive from `TaskBase` or `JobBase`
270
+ 7. **Abstract methods** — all abstract methods (`initialize`, `execute`, `cleanup`) must be implemented
271
+
272
+ If any check fails, the plugin is rejected and the error details are returned.
273
+
274
+ ## Default PVs
275
+
276
+ Every task automatically gets these PVs (prefix: `BEAMLINE:NAMESPACE:TASKNAME`):
277
+
278
+ | PV | Type | Description |
279
+ |---|---|---|
280
+ | `ENABLE` | boolOut | Enable/disable the task |
281
+ | `STATUS` | mbbIn | INIT / RUN / PAUSED / END / ERROR |
282
+ | `MESSAGE` | stringIn | Human-readable status message |
283
+ | `CYCLE_COUNT` | longIn | Cycle counter (continuous mode) |
284
+ | `RUN` | boolOut | Trigger execution (triggered mode) |
285
+
286
+ Every job gets:
287
+
288
+ | PV | Type | Description |
289
+ |---|---|---|
290
+ | `STATUS` | mbbIn | IDLE / RUNNING / SUCCESS / FAILED |
291
+ | `MESSAGE` | stringIn | Human-readable status message |
292
+
293
+ Additional PVs are created from the `pvs` section of `config.yaml`.
294
+
295
+ ## Choosing Between Continuous Task, Triggered Task, and Job
296
+
297
+ | | **Continuous Task** | **Triggered Task** | **Job** |
298
+ |---|---|---|---|
299
+ | **Execution** | `execute()` loops indefinitely | `execute()` called when `RUN` PV is written | `execute()` called via REST |
300
+ | **How triggered** | Automatic (runs on start) | Operator writes `1` to the `RUN` EPICS PV | `POST /api/v1/jobs/{name}/run` |
301
+ | **Return value** | None (side effects only) | None (side effects only) | `JobResult` with `success`, `data`, `message` |
302
+ | **Has `cleanup()`** | Yes | Yes | No |
303
+ | **EPICS PV** | `CYCLE_COUNT` | `RUN` (boolOut) | — |
304
+ | **Typical use** | Polling, monitoring, periodic updates | Operator-driven actions from CS-Studio/Phoebus | API-driven actions from scripts or services |
305
+
306
+ **Rule of thumb:**
307
+ - Use a **continuous task** for anything that needs to run on a regular cycle (e.g., reading a sensor every second).
308
+ - Use a **triggered task** when the action is initiated from the EPICS control system (e.g., an operator clicks a button in Phoebus that writes to a PV).
309
+ - Use a **job** when the action is initiated from software/REST (e.g., a Kubernetes CronJob, a CI script, or another microservice).
310
+
311
+ ## Configuration
312
+
313
+ ### Environment Variables
314
+
315
+ | Variable | Default | Description |
316
+ |---|---|---|
317
+ | `IOCMNG_CONFIG` | (none) | Path to config.yaml |
318
+ | `IOCMNG_BEAMLINE_CONFIG` | (none) | Path to values.yaml |
319
+ | `IOCMNG_PLUGINS_DIR` | `/data/plugins` | Directory for cloned plugins |
320
+ | `IOCMNG_HOST` | `0.0.0.0` | Server bind address |
321
+ | `IOCMNG_PORT` | `8080` | Server port |
322
+ | `IOCMNG_DISABLE_OPHYD` | `true` | Skip ophyd initialization |
323
+ | `IOCMNG_LOG_LEVEL` | `info` | Logging level |
324
+
325
+ ### Optional: Ophyd Device Integration
326
+
327
+ When `ophyd` and `infn_ophyd_hal` are installed and `IOCMNG_DISABLE_OPHYD=false`, the controller automatically creates Ophyd device instances from your `values.yaml` IOC configuration. Tasks can access devices via `self.get_device()` and `self.list_devices()`.
328
+
329
+ ## Project Structure
330
+
331
+ ```
332
+ src/iocmng/
333
+ ├── __init__.py # Package entry: exports TaskBase, JobBase
334
+ ├── base/
335
+ │ ├── task.py # TaskBase — continuous tasks with PV support
336
+ │ └── job.py # JobBase — one-shot jobs with PV support
337
+ ├── core/
338
+ │ ├── controller.py # Central plugin manager
339
+ │ ├── loader.py # Git clone + config loading + module loading
340
+ │ └── validator.py # Plugin validation
341
+ ├── api/
342
+ │ ├── app.py # FastAPI application factory
343
+ │ ├── models.py # Pydantic request/response models
344
+ │ └── routes.py # REST API endpoints
345
+ └── ophyd/
346
+ └── factory.py # Optional ophyd device creation
347
+ ```
348
+
349
+ ## Development
350
+
351
+ ```bash
352
+ # Install in editable mode with dev dependencies
353
+ pip install -e ".[dev]"
354
+
355
+ # Run tests
356
+ pytest tests/ -v
357
+
358
+ # Format
359
+ black .
360
+
361
+ # Lint
362
+ flake8 .
363
+ ```
364
+
365
+ ## GitHub Actions
366
+
367
+ The workflow in `.github/workflows/release.yml` triggers on:
368
+ - **Git tags** matching `v*` (e.g., `v2.0.0`)
369
+ - **Manual dispatch** (workflow_dispatch)
370
+
371
+ It will:
372
+ 1. Run tests
373
+ 2. Build and publish the Python package to PyPI
374
+ 3. Build and push a Docker image to GitHub Container Registry (ghcr.io)
375
+
376
+ ## License
377
+
378
+ MIT
iocmng-2.0.0/README.md ADDED
@@ -0,0 +1,338 @@
1
+ # iocmng — IOC Manager Framework
2
+
3
+ A pluggable task/job framework for IOC Manager applications. Provides base classes for continuous **tasks** and one-shot **jobs** that can be dynamically loaded at runtime via a REST API.
4
+
5
+ ## Features
6
+
7
+ - **`TaskBase`** — base class for continuous tasks (run in a loop)
8
+ - **`JobBase`** — base class for one-shot jobs (run once, return result)
9
+ - **REST API** — add/remove tasks and jobs at runtime from git repositories
10
+ - **Validation** — plugins are validated (must derive from base class, must compile, abstract methods must be implemented)
11
+ - **EPICS soft IOC PVs** — every task and job gets default PVs (STATUS, MESSAGE, etc.) via `softioc`
12
+ - **Per-plugin `config.yaml`** — each plugin defines its PVs and parameters in a config file inside its git repo
13
+ - **Path support** — specify a sub-directory inside the git repo where the plugin sources live
14
+ - **Plugin `requirements.txt`** — plugins can ship their own dependencies
15
+ - **Optional Ophyd integration** — device abstraction via `ophyd`/`infn_ophyd_hal` (optional dependency)
16
+ - **Docker image** — ready-to-run container with the REST API
17
+ - **PyPI package** — `pip install iocmng`
18
+
19
+ ## Quick Start
20
+
21
+ ### Install from PyPI
22
+
23
+ ```bash
24
+ pip install iocmng
25
+
26
+ # With all optional dependencies (ophyd, kubernetes)
27
+ pip install iocmng[all]
28
+ ```
29
+
30
+ ### Run the API Server
31
+
32
+ ```bash
33
+ # Using the CLI entry point
34
+ iocmng-server
35
+
36
+ # Or with environment variables
37
+ IOCMNG_PORT=8080 IOCMNG_LOG_LEVEL=debug iocmng-server
38
+
39
+ # Or with Docker
40
+ docker run -p 8080:8080 ghcr.io/infn-epics/epik8s-beamline-controller:latest
41
+ ```
42
+
43
+ ### Create a Task
44
+
45
+ Create a git repository with:
46
+ 1. A Python file with a class deriving from `TaskBase`
47
+ 2. A `config.yaml` defining PVs and parameters
48
+
49
+ ```
50
+ my-monitor-repo/
51
+ ├── my_monitor.py
52
+ ├── config.yaml
53
+ └── requirements.txt # optional — extra dependencies
54
+ ```
55
+
56
+ **my_monitor.py**
57
+ ```python
58
+ from iocmng import TaskBase
59
+
60
+ class MyMonitor(TaskBase):
61
+ def initialize(self):
62
+ self.logger.info("Starting monitor")
63
+
64
+ def execute(self):
65
+ value = self.read_sensor()
66
+ self.set_pv("READING", value)
67
+ if value > self.parameters.get("threshold", 75):
68
+ self.set_pv("ALARM", 1)
69
+
70
+ def cleanup(self):
71
+ self.logger.info("Stopping monitor")
72
+
73
+ def read_sensor(self):
74
+ return 42.0
75
+ ```
76
+
77
+ **config.yaml**
78
+ ```yaml
79
+ parameters:
80
+ mode: continuous
81
+ interval: 1.0
82
+ threshold: 75.0
83
+
84
+ pvs:
85
+ inputs:
86
+ SETPOINT:
87
+ type: float
88
+ value: 50.0
89
+ unit: "%"
90
+ prec: 2
91
+ low: 0
92
+ high: 100
93
+ outputs:
94
+ READING:
95
+ type: float
96
+ value: 0.0
97
+ unit: "arb"
98
+ prec: 3
99
+ ALARM:
100
+ type: bool
101
+ value: 0
102
+ znam: "OK"
103
+ onam: "ALARM"
104
+ ```
105
+
106
+ ### Create a Job
107
+
108
+ ```python
109
+ # my_diagnostics.py
110
+ from iocmng import JobBase
111
+ from iocmng.base.job import JobResult
112
+
113
+ class MyDiagnostics(JobBase):
114
+ def initialize(self):
115
+ self.logger.info("Preparing diagnostics")
116
+
117
+ def execute(self) -> JobResult:
118
+ info = {"status": "healthy", "uptime": 12345}
119
+ self.set_pv("SYSTEM_NAME", info["status"])
120
+ return JobResult(success=True, data=info, message="Diagnostics OK")
121
+ ```
122
+
123
+ ### REST API Usage
124
+
125
+ #### Add a task
126
+ ```bash
127
+ curl -X POST http://localhost:8080/api/v1/tasks \
128
+ -H "Content-Type: application/json" \
129
+ -d '{
130
+ "name": "my-monitor",
131
+ "git_url": "https://github.com/user/my-monitor-task.git",
132
+ "pat": "ghp_optional_token",
133
+ "branch": "main",
134
+ "path": "src/monitor",
135
+ "parameters": {"threshold": 80.0}
136
+ }'
137
+ ```
138
+
139
+ The `path` field specifies the sub-directory inside the repo where the Python file and `config.yaml` live. Parameters passed in the REST request override values from `config.yaml`.
140
+
141
+ #### Remove a task
142
+ ```bash
143
+ curl -X DELETE http://localhost:8080/api/v1/tasks/my-monitor
144
+ ```
145
+
146
+ #### Add a job
147
+ ```bash
148
+ curl -X POST http://localhost:8080/api/v1/jobs \
149
+ -H "Content-Type: application/json" \
150
+ -d '{
151
+ "name": "my-diag",
152
+ "git_url": "https://github.com/user/my-diagnostics-job.git",
153
+ "path": "jobs/diagnostics"
154
+ }'
155
+ ```
156
+
157
+ #### Run a job
158
+ ```bash
159
+ curl -X POST http://localhost:8080/api/v1/jobs/my-diag/run
160
+ ```
161
+
162
+ #### Remove a job
163
+ ```bash
164
+ curl -X DELETE http://localhost:8080/api/v1/jobs/my-diag
165
+ ```
166
+
167
+ #### List all tasks
168
+ ```bash
169
+ curl http://localhost:8080/api/v1/tasks
170
+ ```
171
+
172
+ #### Health check
173
+ ```bash
174
+ curl http://localhost:8080/api/v1/health
175
+ ```
176
+
177
+ ## Plugin Structure
178
+
179
+ Each plugin lives in a git repository (or a sub-directory of one). The expected layout:
180
+
181
+ ```
182
+ <repo-root>/
183
+ └── <path>/ # optional sub-directory (specified via REST "path" field)
184
+ ├── my_plugin.py # Python module with TaskBase/JobBase subclass
185
+ ├── config.yaml # Plugin configuration (PVs, parameters)
186
+ └── requirements.txt # Optional additional pip dependencies
187
+ ```
188
+
189
+ ### config.yaml Format
190
+
191
+ ```yaml
192
+ # Parameters — passed to the plugin constructor as self.parameters
193
+ # REST-supplied parameters override these defaults
194
+ parameters:
195
+ mode: continuous # "continuous" or "triggered"
196
+ interval: 1.0 # application-specific
197
+ threshold: 75.0 # application-specific
198
+
199
+ # PV definitions — created automatically by the IOC Manager
200
+ pvs:
201
+ inputs: # writable PVs (operator → plugin)
202
+ SETPOINT:
203
+ type: float # float, int, string, bool
204
+ value: 50.0 # initial value
205
+ unit: "%" # EGU (float only)
206
+ prec: 2 # precision (float only)
207
+ low: 0 # LOPR (float only)
208
+ high: 100 # HOPR (float only)
209
+ outputs: # read-only PVs (plugin → operator)
210
+ READING:
211
+ type: float
212
+ value: 0.0
213
+ ALARM:
214
+ type: bool
215
+ value: 0
216
+ znam: "OK" # zero-state name (bool only)
217
+ onam: "ALARM" # one-state name (bool only)
218
+ ```
219
+
220
+ ## Plugin Validation
221
+
222
+ When a task or job is added, the framework performs the following checks:
223
+
224
+ 1. **Clone** — the git repository is cloned (with optional PAT for private repos)
225
+ 2. **Dependencies** — `requirements.txt` is installed if present (from `path` or repo root)
226
+ 3. **Config** — `config.yaml` is loaded from `path` to read PV definitions and default parameters
227
+ 4. **Syntax** — Python files are parsed via AST for syntax errors
228
+ 5. **Import** — the module is imported to check for runtime import errors
229
+ 6. **Inheritance** — at least one class must derive from `TaskBase` or `JobBase`
230
+ 7. **Abstract methods** — all abstract methods (`initialize`, `execute`, `cleanup`) must be implemented
231
+
232
+ If any check fails, the plugin is rejected and the error details are returned.
233
+
234
+ ## Default PVs
235
+
236
+ Every task automatically gets these PVs (prefix: `BEAMLINE:NAMESPACE:TASKNAME`):
237
+
238
+ | PV | Type | Description |
239
+ |---|---|---|
240
+ | `ENABLE` | boolOut | Enable/disable the task |
241
+ | `STATUS` | mbbIn | INIT / RUN / PAUSED / END / ERROR |
242
+ | `MESSAGE` | stringIn | Human-readable status message |
243
+ | `CYCLE_COUNT` | longIn | Cycle counter (continuous mode) |
244
+ | `RUN` | boolOut | Trigger execution (triggered mode) |
245
+
246
+ Every job gets:
247
+
248
+ | PV | Type | Description |
249
+ |---|---|---|
250
+ | `STATUS` | mbbIn | IDLE / RUNNING / SUCCESS / FAILED |
251
+ | `MESSAGE` | stringIn | Human-readable status message |
252
+
253
+ Additional PVs are created from the `pvs` section of `config.yaml`.
254
+
255
+ ## Choosing Between Continuous Task, Triggered Task, and Job
256
+
257
+ | | **Continuous Task** | **Triggered Task** | **Job** |
258
+ |---|---|---|---|
259
+ | **Execution** | `execute()` loops indefinitely | `execute()` called when `RUN` PV is written | `execute()` called via REST |
260
+ | **How triggered** | Automatic (runs on start) | Operator writes `1` to the `RUN` EPICS PV | `POST /api/v1/jobs/{name}/run` |
261
+ | **Return value** | None (side effects only) | None (side effects only) | `JobResult` with `success`, `data`, `message` |
262
+ | **Has `cleanup()`** | Yes | Yes | No |
263
+ | **EPICS PV** | `CYCLE_COUNT` | `RUN` (boolOut) | — |
264
+ | **Typical use** | Polling, monitoring, periodic updates | Operator-driven actions from CS-Studio/Phoebus | API-driven actions from scripts or services |
265
+
266
+ **Rule of thumb:**
267
+ - Use a **continuous task** for anything that needs to run on a regular cycle (e.g., reading a sensor every second).
268
+ - Use a **triggered task** when the action is initiated from the EPICS control system (e.g., an operator clicks a button in Phoebus that writes to a PV).
269
+ - Use a **job** when the action is initiated from software/REST (e.g., a Kubernetes CronJob, a CI script, or another microservice).
270
+
271
+ ## Configuration
272
+
273
+ ### Environment Variables
274
+
275
+ | Variable | Default | Description |
276
+ |---|---|---|
277
+ | `IOCMNG_CONFIG` | (none) | Path to config.yaml |
278
+ | `IOCMNG_BEAMLINE_CONFIG` | (none) | Path to values.yaml |
279
+ | `IOCMNG_PLUGINS_DIR` | `/data/plugins` | Directory for cloned plugins |
280
+ | `IOCMNG_HOST` | `0.0.0.0` | Server bind address |
281
+ | `IOCMNG_PORT` | `8080` | Server port |
282
+ | `IOCMNG_DISABLE_OPHYD` | `true` | Skip ophyd initialization |
283
+ | `IOCMNG_LOG_LEVEL` | `info` | Logging level |
284
+
285
+ ### Optional: Ophyd Device Integration
286
+
287
+ When `ophyd` and `infn_ophyd_hal` are installed and `IOCMNG_DISABLE_OPHYD=false`, the controller automatically creates Ophyd device instances from your `values.yaml` IOC configuration. Tasks can access devices via `self.get_device()` and `self.list_devices()`.
288
+
289
+ ## Project Structure
290
+
291
+ ```
292
+ src/iocmng/
293
+ ├── __init__.py # Package entry: exports TaskBase, JobBase
294
+ ├── base/
295
+ │ ├── task.py # TaskBase — continuous tasks with PV support
296
+ │ └── job.py # JobBase — one-shot jobs with PV support
297
+ ├── core/
298
+ │ ├── controller.py # Central plugin manager
299
+ │ ├── loader.py # Git clone + config loading + module loading
300
+ │ └── validator.py # Plugin validation
301
+ ├── api/
302
+ │ ├── app.py # FastAPI application factory
303
+ │ ├── models.py # Pydantic request/response models
304
+ │ └── routes.py # REST API endpoints
305
+ └── ophyd/
306
+ └── factory.py # Optional ophyd device creation
307
+ ```
308
+
309
+ ## Development
310
+
311
+ ```bash
312
+ # Install in editable mode with dev dependencies
313
+ pip install -e ".[dev]"
314
+
315
+ # Run tests
316
+ pytest tests/ -v
317
+
318
+ # Format
319
+ black .
320
+
321
+ # Lint
322
+ flake8 .
323
+ ```
324
+
325
+ ## GitHub Actions
326
+
327
+ The workflow in `.github/workflows/release.yml` triggers on:
328
+ - **Git tags** matching `v*` (e.g., `v2.0.0`)
329
+ - **Manual dispatch** (workflow_dispatch)
330
+
331
+ It will:
332
+ 1. Run tests
333
+ 2. Build and publish the Python package to PyPI
334
+ 3. Build and push a Docker image to GitHub Container Registry (ghcr.io)
335
+
336
+ ## License
337
+
338
+ MIT