Skip to content

Commit 8db46ae

Browse files
Merge pull request #1 from ISISComputingGroup/add_ci
add required stuff for polref test
2 parents 2d4292c + 995e016 commit 8db46ae

File tree

8 files changed

+225
-49
lines changed

8 files changed

+225
-49
lines changed

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "pip"
4+
directory: "/"
5+
schedule:
6+
interval: "daily"
7+
- package-ecosystem: "github-actions"
8+
directory: "/"
9+
schedule:
10+
interval: "daily"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Lint-and-test
2+
on: [push, pull_request, workflow_call]
3+
jobs:
4+
call-linter-workflow:
5+
uses: ISISComputingGroup/reusable-workflows/.github/workflows/linters.yml@main
6+
with:
7+
compare-branch: origin/main
8+
python-ver: '3.12'
9+
runs-on: windows-latest
10+
tests:
11+
runs-on: windows-latest
12+
strategy:
13+
matrix:
14+
version: ['3.12']
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-python@v5
18+
with:
19+
python-version: ${{ matrix.version }}
20+
- name: install requirements
21+
run: pip install -e .[dev]
22+
- name: run unit tests
23+
run: python -m pytest .
24+
results:
25+
if: ${{ always() }}
26+
runs-on: ubuntu-latest
27+
name: Final Results
28+
needs: [call-linter-workflow, tests]
29+
steps:
30+
- run: exit 1
31+
# see https://stackoverflow.com/a/67532120/4907315
32+
if: >-
33+
${{
34+
contains(needs.*.result, 'failure')
35+
|| contains(needs.*.result, 'cancelled')
36+
}}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: lint-and-test-nightly
2+
on:
3+
schedule:
4+
- cron: "31 0 * * *"
5+
6+
jobs:
7+
lint-and-test-nightly:
8+
uses: ./.github/workflows/Lint-and-test.yml

.github/workflows/release.yml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: Publish Python distribution to PyPI
2+
on: push
3+
jobs:
4+
lint-and-test:
5+
if: github.ref_type == 'tag'
6+
name: Run linter and tests
7+
uses: ./.github/workflows/Lint-and-test.yml
8+
build:
9+
needs: lint-and-test
10+
if: github.ref_type == 'tag'
11+
name: build distribution
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.11"
20+
- name: Install pypa/build
21+
run: >-
22+
python3 -m
23+
pip install
24+
build
25+
--user
26+
- name: Build a binary wheel and a source tarball
27+
run: python3 -m build
28+
- name: Store the distribution packages
29+
uses: actions/upload-artifact@v4
30+
with:
31+
name: python-package-distributions
32+
path: dist/
33+
publish-to-pypi:
34+
name: >-
35+
Publish Python distribution to PyPI
36+
if: github.ref_type == 'tag'
37+
needs: [lint-and-test, build]
38+
runs-on: ubuntu-latest
39+
environment:
40+
name: release
41+
url: https://pypi.org/p/lewis
42+
permissions:
43+
id-token: write # IMPORTANT: mandatory for trusted publishing
44+
steps:
45+
- name: Download all the dists
46+
uses: actions/download-artifact@v4
47+
with:
48+
name: python-package-distributions
49+
path: dist/
50+
- name: Publish distribution to PyPI
51+
uses: pypa/gh-action-pypi-publish@release/v1
52+
github-release:
53+
name: >-
54+
Sign the Python distribution with Sigstore
55+
and upload them to GitHub Release
56+
needs: [lint-and-test, build, publish-to-pypi]
57+
runs-on: ubuntu-latest
58+
59+
permissions:
60+
contents: write # IMPORTANT: mandatory for making GitHub Releases
61+
id-token: write # IMPORTANT: mandatory for sigstore
62+
63+
steps:
64+
- name: Download all the dists
65+
uses: actions/download-artifact@v4
66+
with:
67+
name: python-package-distributions
68+
path: dist/
69+
- name: Sign the dists with Sigstore
70+
uses: sigstore/[email protected]
71+
with:
72+
inputs: >-
73+
./dist/*.tar.gz
74+
./dist/*.whl
75+
- name: Create GitHub Release
76+
env:
77+
GITHUB_TOKEN: ${{ github.token }}
78+
run: >-
79+
gh release create
80+
'${{ github.ref_name }}'
81+
--repo '${{ github.repository }}'
82+
--notes ""
83+
- name: Upload artifact signatures to GitHub Release
84+
env:
85+
GITHUB_TOKEN: ${{ github.token }}
86+
# Upload to GitHub Release using the `gh` CLI.
87+
# `dist/` contains the built packages, and the
88+
# sigstore-produced signatures and certificates.
89+
run: >-
90+
gh release upload
91+
'${{ github.ref_name }}' dist/**
92+
--repo '${{ github.repository }}'

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Borzoi
22

3-
Borzoi like running. This listens to ISISDAE's run state and pushes start/stop Flatbuffer blobs to Kafka accordingly.
3+
Borzoi like running. This listens to ISISDAE's start/stop times and pushes start/stop Flatbuffer blobs to Kafka accordingly.

main.py

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import os
2-
import uuid
3-
import logging
41
import asyncio
52
import json
3+
import logging
4+
import os
5+
import uuid
66

7-
from aioca import caget, camonitor
7+
from aioca import camonitor
88
from aiokafka import AIOKafkaProducer
9-
from epicscorelibs.ca.dbr import DBR_CHAR_BYTES
10-
from ibex_non_ca_helpers.hex import dehex_decompress_and_dejson
11-
9+
from epicscorelibs.ca.dbr import DBR_CHAR_BYTES, ca_bytes
10+
from ibex_non_ca_helpers.compress_hex import dehex_decompress_and_dejson
1211
from streaming_data_types.run_start_pl72 import serialise_pl72
1312
from streaming_data_types.run_stop_6s4t import serialise_6s4t
1413

@@ -19,51 +18,74 @@
1918
class RunStarter:
2019
def __init__(
2120
self, prefix: str, instrument_name: str, producer: AIOKafkaProducer, topic: str
22-
):
21+
) -> None:
2322
self.producer = None
2423
self.prefix = prefix
2524
self.blocks = []
2625
self.current_job_id = ""
2726
self.producer = producer
2827
self.instrument_name = instrument_name
2928
self.topic = topic
29+
self.current_run_number = None
30+
self.current_start_time = None
31+
self.current_stop_time = None
3032

31-
async def set_up_monitors(self):
33+
async def set_up_monitors(self) -> None:
3234
logger.info("Setting up monitors")
3335
camonitor(
3436
f"{self.prefix}CS:BLOCKSERVER:BLOCKNAMES",
3537
callback=self._update_blocks,
38+
all_updates=True,
3639
datatype=DBR_CHAR_BYTES,
3740
)
3841
camonitor(
39-
f"{self.prefix}DAE:RUNSTATE",
40-
callback=self._react_to_runstate_change,
42+
f"{self.prefix}DAE:RUNNUMBER",
43+
callback=self._update_run_number,
4144
all_updates=True,
4245
datatype=str,
4346
)
47+
camonitor(
48+
f"{self.prefix}DAE:START_TIME",
49+
callback=self.construct_and_send_runstart,
50+
all_updates=True,
51+
datatype=float,
52+
)
53+
camonitor(
54+
f"{self.prefix}DAE:STOP_TIME",
55+
self.construct_and_send_runstop,
56+
all_updates=True,
57+
datatype=float,
58+
)
59+
60+
def _update_run_number(self, value: int) -> None:
61+
# Cache this as we want the run start message construction and production to be as fast as
62+
# possible so we don't miss events
63+
logger.info(f"Run number updated to {value}")
64+
self.current_run_number = value
4465

45-
def _update_blocks(self, value):
66+
def _update_blocks(self, value: ca_bytes) -> None:
4667
logger.debug(f"blocks_hexed: {value}")
4768
blocks_unhexed = dehex_decompress_and_dejson(bytes(value))
4869
logger.debug(f"blocks_unhexed: {blocks_unhexed}")
4970
self.blocks = [f"{self.prefix}CS:SB:{x}" for x in blocks_unhexed]
5071

51-
async def _react_to_runstate_change(self, value):
52-
logger.info(f"Runstate changed to {value}")
72+
async def construct_and_send_runstart(self, value: float | None) -> None:
73+
if self.current_start_time is None:
74+
logger.info("Initial update for start time - not sending run start")
75+
self.current_start_time = value
76+
return
5377

54-
if value == "BEGINNING":
55-
self.current_job_id = str(uuid.uuid4())
56-
await self.construct_and_send_runstart(self.current_job_id)
57-
elif value == "ENDING":
58-
await self.construct_and_send_runstop(self.current_job_id)
78+
if value == self.current_start_time or value is None:
79+
logger.error("run start time is the same as cached or invalid. ignoring update")
80+
return
5981

60-
async def construct_and_send_runstart(self, job_id: str):
61-
logger.info(f"Sending run start with job_id: {job_id}")
62-
start_time_s = await caget(f"{self.prefix}DAE:START_TIME")
63-
start_time_ms = int(start_time_s * 1000)
82+
self.current_start_time = value
83+
self.current_job_id = str(uuid.uuid4())
84+
logger.info(f"Sending run start with job_id: {self.current_job_id}")
85+
start_time_ms = int(self.current_start_time) * 1000
6486
logger.info(f"Start time: {start_time_ms}")
6587

66-
runnum = await caget(f"{self.prefix}DAE:RUNNUMBER")
88+
runnum = self.current_run_number
6789

6890
nexus_structure = {
6991
"children": [
@@ -109,31 +131,39 @@ async def construct_and_send_runstart(self, job_id: str):
109131
filename = f"{self.instrument_name}{runnum}.nxs"
110132

111133
blob = serialise_pl72(
112-
job_id,
134+
self.current_job_id,
113135
filename=filename,
114136
start_time=start_time_ms,
115137
nexus_structure=json.dumps(nexus_structure),
138+
run_name=runnum,
139+
instrument_name=self.instrument_name,
116140
)
117141
await self.producer.send(self.topic, blob)
142+
logger.info(f"Sent {blob} blob")
118143

119-
async def construct_and_send_runstop(self, job_id: str):
120-
logger.info(f"Sending run stop with job_id: {job_id}")
121-
# stop_time only gets set to a non-zero value when the runstate goes back to SETUP.
122-
# This is dirty, but poll it every 0.5 seconds until it does.
123-
while (
124-
current_runstate := await caget(f"{self.prefix}DAE:RUNSTATE", datatype=str)
125-
!= "SETUP"
126-
):
127-
logger.debug(
128-
f"Waiting for run state to go back to SETUP. Currently {current_runstate}"
129-
)
130-
await asyncio.sleep(0.5)
131-
132-
stop_time_s = await caget(f"{self.prefix}DAE:STOP_TIME")
133-
stop_time_ms = int(stop_time_s * 1000)
144+
async def construct_and_send_runstop(self, value: float) -> None:
145+
if self.current_stop_time is None:
146+
self.current_stop_time = value
147+
logger.info("Initial update for stop time - not sending run stop")
148+
return
149+
150+
if value == self.current_stop_time or value is None:
151+
logger.error("run stop time is the same as cached or invalid")
152+
return
153+
154+
if value == 0:
155+
# This happens when a new run starts
156+
logger.debug("stop time set to 0")
157+
self.current_stop_time = value
158+
return
159+
160+
self.current_stop_time = value
161+
logger.info(f"Sending run stop with job_id: {self.current_job_id}")
162+
163+
stop_time_ms = int(value * 1000)
134164
logger.info(f"stop time: {stop_time_ms}")
135165
blob = serialise_6s4t(
136-
job_id, stop_time=stop_time_ms, command_id=self.current_job_id
166+
self.current_job_id, stop_time=stop_time_ms, command_id=self.current_job_id
137167
)
138168
await self.producer.send(self.topic, blob)
139169

@@ -144,17 +174,15 @@ async def set_up_producer(broker: str) -> AIOKafkaProducer:
144174
return producer
145175

146176

147-
def main():
177+
def main() -> None:
148178
prefix = os.environ.get("MYPVPREFIX")
149179
instrument_name = os.environ.get("INSTRUMENT")
150180

151181
if prefix is None or instrument_name is None:
152-
raise ValueError(
153-
"prefix or instrument name not set - have you run config_env.bat?"
154-
)
182+
raise ValueError("prefix or instrument name not set - have you run config_env.bat?")
155183

156-
broker = "livedata.isis.cclrc.ac.uk:31092"
157-
topic = f"{instrument_name}_runInfo"
184+
broker = os.environ.get("BORZOI_KAFKA_BROKER", "livedata.isis.cclrc.ac.uk:31092")
185+
topic = os.environ.get("BORZOI_TOPIC", f"{instrument_name}_runInfo")
158186
logger.info("setting up producer")
159187
loop = asyncio.new_event_loop()
160188
producer = loop.run_until_complete(set_up_producer(broker))

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ dependencies = [
1212
]
1313

1414
[project.optional-dependencies]
15-
dev = ["ruff"]
15+
dev = ["ruff", "pytest", "pyright"]

tests/test_main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def test_that_does_nothing():
2+
pass

0 commit comments

Comments
 (0)