Skip to content

Commit f7af723

Browse files
Rework after doing the same split as poc
1 parent 0c1a98c commit f7af723

109 files changed

Lines changed: 2994 additions & 1021 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/python.yaml

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ jobs:
1818

1919
steps:
2020
- uses: actions/checkout@v4
21-
with:
22-
sparse-checkout: backend
2321

2422
- uses: actions/setup-python@v5
2523
with:
@@ -28,17 +26,62 @@ jobs:
2826
- uses: astral-sh/setup-uv@v5
2927

3028
- name: Install backend
31-
run: uv sync
29+
run: uv sync --package acidwatch-api
3230

3331
- name: Run ruff
3432
run: |
35-
uv run ruff format --check
36-
uv run ruff check
33+
uv run --package acidwatch-api ruff format --check src tests packages ../workers
34+
uv run --package acidwatch-api ruff check src tests packages ../workers
3735
3836
- name: Run mypy
39-
run: uv run mypy --strict src
37+
run: uv run --package acidwatch-api mypy --strict src packages/acidwatch-models/src packages/acidwatch-messaging/src
4038

4139
- name: Run pytest
4240
run: |
4341
cp .env.example .env
44-
uv run pytest tests
42+
uv run --package acidwatch-api pytest tests
43+
44+
worker-packages:
45+
runs-on: ubuntu-latest
46+
strategy:
47+
matrix:
48+
include:
49+
- package: acidwatch-worker-example
50+
test_path: workers/example/tests
51+
- package: acidwatch-worker-tocomo
52+
test_path: workers/tocomo/tests
53+
- package: acidwatch-worker-arcs
54+
test_path: ""
55+
- package: acidwatch-worker-arcs-exp
56+
test_path: workers/arcs-exp/tests
57+
- package: acidwatch-worker-solubilityccs
58+
test_path: ""
59+
- package: acidwatch-worker-gibbs-minimization
60+
test_path: workers/gibbs-minimization/tests
61+
- package: acidwatch-worker-phpitz-reactive
62+
test_path: ""
63+
- package: acidwatch-worker-phpitz-solubility
64+
test_path: ""
65+
66+
steps:
67+
- uses: actions/checkout@v4
68+
69+
- uses: actions/setup-python@v5
70+
with:
71+
python-version: '3.12'
72+
73+
- uses: astral-sh/setup-uv@v5
74+
75+
- name: Install worker
76+
run: uv sync --package ${{ matrix.package }}
77+
78+
- name: Build worker package
79+
run: uv build --package ${{ matrix.package }}
80+
81+
- name: Install Tocomo calculation package
82+
if: matrix.package == 'acidwatch-worker-tocomo'
83+
run: uv pip install --no-deps "git+https://github.com/equinor/tocomo@74e23131bd27d63dcf71b4a62eb81cdb76d981c9#subdirectory=backend"
84+
85+
- name: Test worker
86+
if: matrix.test_path != ''
87+
run: uv run --package ${{ matrix.package }} pytest ${{ matrix.test_path }}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[project]
2+
name = "acidwatch-messaging"
3+
version = "0.1.0"
4+
description = "Broker transports and worker runtime for AcidWatch models"
5+
requires-python = ">=3.12, <3.14"
6+
dependencies = [
7+
"acidwatch-models",
8+
"aio-pika>=10.0.1",
9+
"azure-servicebus>=7.14.2,<8.0.0",
10+
]
11+
12+
[build-system]
13+
requires = ["hatchling"]
14+
build-backend = "hatchling.build"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from .contracts import AdapterJob, AdapterResult, Heartbeat
2+
from .queues import (
3+
DEAD_LETTER_QUEUE,
4+
HEARTBEATS_QUEUE,
5+
RESULTS_QUEUE,
6+
job_queue_name,
7+
)
8+
from .transport import (
9+
AzureServiceBusTransport,
10+
Message,
11+
RabbitMQTransport,
12+
Transport,
13+
create_transport,
14+
)
15+
from .worker import (
16+
AdapterWorker,
17+
run_adapter_job,
18+
run_worker,
19+
run_worker_from_environment,
20+
)
21+
22+
__all__ = [
23+
"AdapterJob",
24+
"AdapterResult",
25+
"AdapterWorker",
26+
"AzureServiceBusTransport",
27+
"DEAD_LETTER_QUEUE",
28+
"HEARTBEATS_QUEUE",
29+
"Heartbeat",
30+
"Message",
31+
"RESULTS_QUEUE",
32+
"RabbitMQTransport",
33+
"Transport",
34+
"create_transport",
35+
"job_queue_name",
36+
"run_adapter_job",
37+
"run_worker",
38+
"run_worker_from_environment",
39+
]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from datetime import datetime
2+
from uuid import UUID
3+
4+
from pydantic import BaseModel, ConfigDict, Field
5+
6+
from acidwatch_models.datamodel import AnyPanel, Conditions, Phase
7+
8+
9+
class _Message(BaseModel):
10+
model_config = ConfigDict(extra="forbid")
11+
12+
13+
class AdapterJob(_Message):
14+
model_input_id: UUID
15+
model_id: str
16+
concentrations: dict[str, int | float]
17+
parameters: dict[str, bool | float | int | str]
18+
conditions: Conditions
19+
20+
21+
class AdapterResult(_Message):
22+
model_input_id: UUID
23+
phases: list[Phase] = Field(default_factory=list)
24+
panels: list[AnyPanel] = Field(default_factory=list)
25+
error: str | None = None
26+
27+
28+
class Heartbeat(_Message):
29+
model_id: str
30+
instance_id: str
31+
timestamp: datetime
32+
job_id: str | None = None

backend/src/acidwatch_api/broker/queues.py renamed to backend/packages/acidwatch-messaging/src/acidwatch_messaging/queues.py

File renamed without changes.

backend/src/acidwatch_api/broker/transport.py renamed to backend/packages/acidwatch-messaging/src/acidwatch_messaging/transport.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import asyncio
45
from collections.abc import AsyncIterator
56
from types import TracebackType
67
from typing import Any, Protocol, Self
@@ -9,8 +10,18 @@
910
from azure.servicebus import ServiceBusMessage
1011
from azure.servicebus.aio import AutoLockRenewer, ServiceBusClient
1112
from pamqp.common import FieldTable
13+
from pydantic import BaseModel
1214

13-
from acidwatch_api.broker.queues import DEAD_LETTER_QUEUE, RESULTS_QUEUE
15+
from acidwatch_messaging.queues import DEAD_LETTER_QUEUE, RESULTS_QUEUE
16+
17+
MESSAGE_LOCK_RENEWAL_SECONDS = 7200
18+
Payload = BaseModel | dict[str, Any]
19+
20+
21+
def _payload_json(payload: Payload) -> str:
22+
if isinstance(payload, BaseModel):
23+
return payload.model_dump_json()
24+
return json.dumps(payload)
1425

1526

1627
class Message(Protocol):
@@ -22,7 +33,7 @@ async def reject(self) -> None: ...
2233

2334

2435
class Transport(Protocol):
25-
async def publish(self, queue_name: str, payload: dict[str, Any]) -> None: ...
36+
async def publish(self, queue_name: str, payload: Payload) -> None: ...
2637

2738
def subscribe(self, queue_name: str) -> AsyncIterator[Message]: ...
2839

@@ -85,13 +96,13 @@ async def _declare_queues(
8596
)
8697

8798
async def publish(
88-
self, queue_name: str, payload: dict[str, Any]
99+
self, queue_name: str, payload: Payload
89100
) -> None:
90101
if self._publish_channel is None:
91102
raise RuntimeError("Transport is not open")
92103
await self._publish_channel.default_exchange.publish(
93104
aio_pika.Message(
94-
body=json.dumps(payload).encode(),
105+
body=_payload_json(payload).encode(),
95106
delivery_mode=aio_pika.DeliveryMode.PERSISTENT,
96107
content_type="application/json",
97108
),
@@ -131,6 +142,8 @@ class AzureServiceBusTransport:
131142
def __init__(self, connection_string: str, queue_names: list[str]):
132143
self._connection_string = connection_string
133144
self._client: ServiceBusClient | None = None
145+
self._senders: dict[str, Any] = {}
146+
self._sender_locks: dict[str, asyncio.Lock] = {}
134147

135148
async def __aenter__(self) -> Self:
136149
self._client = ServiceBusClient.from_connection_string(
@@ -145,18 +158,26 @@ async def __aexit__(
145158
traceback: TracebackType | None,
146159
) -> None:
147160
if self._client is not None:
161+
for sender in self._senders.values():
162+
await sender.close()
163+
self._senders.clear()
164+
self._sender_locks.clear()
148165
await self._client.close()
149166

150167
async def publish(
151-
self, queue_name: str, payload: dict[str, Any]
168+
self, queue_name: str, payload: Payload
152169
) -> None:
153170
if self._client is None:
154171
raise RuntimeError("Transport is not open")
155-
sender = self._client.get_queue_sender(queue_name)
156-
async with sender:
172+
sender = self._senders.get(queue_name)
173+
if sender is None:
174+
sender = self._client.get_queue_sender(queue_name)
175+
self._senders[queue_name] = sender
176+
self._sender_locks[queue_name] = asyncio.Lock()
177+
async with self._sender_locks[queue_name]:
157178
await sender.send_messages(
158179
ServiceBusMessage(
159-
json.dumps(payload),
180+
_payload_json(payload),
160181
content_type="application/json",
161182
)
162183
)
@@ -169,8 +190,21 @@ async def subscribe(self, queue_name: str) -> AsyncIterator[Message]:
169190
prefetch_count=1,
170191
)
171192
async with receiver, AutoLockRenewer(
172-
max_lock_renewal_duration=7200
193+
max_lock_renewal_duration=MESSAGE_LOCK_RENEWAL_SECONDS
173194
) as renewer:
174195
async for message in receiver:
175196
renewer.register(receiver, message)
176197
yield AzureServiceBusMessage(message, receiver)
198+
199+
200+
def create_transport(
201+
broker_url: str,
202+
queue_names: list[str],
203+
backend: str = "",
204+
) -> RabbitMQTransport | AzureServiceBusTransport:
205+
selected = backend.strip().lower()
206+
if selected in {"azure_service_bus", "servicebus"} or (
207+
not selected and broker_url.startswith("Endpoint=")
208+
):
209+
return AzureServiceBusTransport(broker_url, queue_names)
210+
return RabbitMQTransport(broker_url, queue_names)

0 commit comments

Comments
 (0)