Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ updates:
groups:
python:
patterns: ["*"]
- package-ecosystem: gomod
directory: /gen/go
schedule:
interval: weekly
- package-ecosystem: docker
directory: /
schedule:
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}

- name: Authenticate at registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
Expand All @@ -38,6 +40,7 @@ jobs:
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
context: git
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,prefix=,format=short
Expand All @@ -56,3 +59,13 @@ jobs:
provenance: true
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Setup Buf
uses: bufbuild/buf-action@85aebf73123b5c15fd5528aaecbf9129cddf7fa7 # v1.6.0
with:
version: 1.73.0
setup_only: true
token: ${{ secrets.BUF_TOKEN }}

- name: Publish schema
run: buf push --create --create-visibility public --label main --git-metadata
29 changes: 17 additions & 12 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,32 @@ jobs:
enable-cache: true
cache-dependency-glob: uv.lock

- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: gen/go/go.mod
cache-dependency-path: gen/go/go.sum

- name: Install dependencies
run: uv sync --locked

- name: Setup Buf
uses: bufbuild/buf-action@85aebf73123b5c15fd5528aaecbf9129cddf7fa7 # v1.6.0
with:
version: 1.73.0
setup_only: true
token: ${{ secrets.BUF_TOKEN }}

- name: Lint and format
run: |
buf lint
buf format --diff --exit-code --path proto/fanscore/optimizer
uv run --locked ruff check
uv run --locked ruff format --check

- name: Check generated bindings
run: |
uv run --locked python scripts/generate.py --check
uv run --locked python scripts/generate.py --go --check
generated="$(mktemp -d)"
trap 'rm -rf "$generated"' EXIT
buf generate --output "$generated"
diff -ru -x __pycache__ src/fanscore/optimizer/v1 "$generated/src/fanscore/optimizer/v1"

- name: Test Python service
run: uv run --locked pytest
- name: Check protobuf compatibility
run: buf breaking --exclude-imports --against buf.build/fanscore-ch/optimizer:main

- name: Test Go client against Python
run: uv run --locked python scripts/test_go.py
- name: Test
run: uv run --locked pytest
57 changes: 25 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,48 @@
# Optimizer

FanScore's stateless gRPC service for OR-Tools CP-SAT models. Platform builds
models and handles business rules and transactions; Optimizer executes solves.
models and handles business rules; Optimizer executes solves.

## Prerequisites
## Run

- [devenv](https://devenv.sh/)

## Quick Start
Install [devenv](https://devenv.sh/), then:

```sh
devenv up
```

Devenv supplies Python 3.14 and Go and installs the locked dependencies.
The service listens on port `50051`; the default binding is `[::]:50051`.
Configuration comes from environment variables documented in [`.env.example`](.env.example).
Devenv supplies Python, Buf, and the locked dependencies.
The service listens on port `50051`. See [`.env.example`](.env.example) for configuration.

## Development

```sh
devenv shell -- generate # Python bindings
devenv shell -- generate-go # Go bindings
devenv shell -- check-generated # Verify generated bindings
devenv shell -- generate # Generate Python service bindings with Buf
devenv shell -- check-generated # Check bindings match the schema
devenv shell -- lint
devenv shell -- test # Python tests
devenv shell -- test-go # Go client against a temporary Python server
devenv shell -- test
devenv shell -- health # Probe the running service
```

CI runs the same checks and also builds and verifies the container.

## API
After changing the [service schema](proto/fanscore/optimizer/v1/optimizer.proto),
run `generate` and commit the updated Python bindings with the schema.
CI checks that bindings match and that the contract remains compatible with BSR `main`.

[`OptimizerService.Solve`](proto/optimizer/v1/optimizer.proto) accepts an OR-Tools
model and supported solver parameters and returns its solver response. Only use
solutions with status `OPTIMAL` or `FEASIBLE`. Set an RPC deadline; server limits
cap solve time, workers, concurrent solves, and message size.
The service reuses OR-Tools' installed Python messages. Vendored schemas in
`proto/ortools` must match the installed OR-Tools version; descriptor tests enforce this.

Python bindings live in `src/optimizer/v1`. The generated Go module lives in
`gen/go`; Platform imports it at a pinned version. Upstream protobufs and OR-Tools
versions must match; [descriptor tests](tests/test_proto.py) verify this.
## Smoke test

## Project Structure
With the service running:

```text
src/optimizer/ # gRPC server, configuration, and solver
proto/ # Service contract and vendored OR-Tools protos
gen/go/ # Generated Go client and OR-Tools messages
scripts/ # Binding generation and Go integration runner
tests/ # Python tests
devenv.nix # Toolchain, scripts, process, and hooks
Dockerfile # Production image
```sh
buf curl --schema . --protocol grpc --http2-prior-knowledge \
-d '{"model":{}}' \
http://127.0.0.1:50051/fanscore.optimizer.v1.OptimizerService/Solve
```

## API and SDKs

See the [contract documentation](proto/buf.md) for RPC behavior and supported parameters.
Generated client SDKs are available from
[`buf.build/fanscore-ch/optimizer`](https://buf.build/fanscore-ch/optimizer).
12 changes: 12 additions & 0 deletions buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: v2
plugins:
- remote: buf.build/protocolbuffers/python:v33.5
revision: 1
out: src
- remote: buf.build/grpc/python:v1.81.1
revision: 1
out: src
inputs:
- directory: .
paths:
- proto/fanscore/optimizer
15 changes: 15 additions & 0 deletions buf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: v2
modules:
- path: proto
name: buf.build/fanscore-ch/optimizer
lint:
use:
- STANDARD
- COMMENTS
- UNARY_RPC
disallow_comment_ignores: true
ignore:
- proto/ortools
breaking:
use:
- FILE
30 changes: 19 additions & 11 deletions devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,29 @@
};
};

languages.go.enable = true;

packages = [ pkgs.git ];
packages = [
pkgs.git
pkgs.buf
pkgs.diffutils
];

scripts = {
generate.exec = "uv run python scripts/generate.py";
generate-go.exec = "uv run python scripts/generate.py --go";
generate.exec = "buf generate";
check-generated.exec = ''
set -e
uv run python scripts/generate.py --check
uv run python scripts/generate.py --go --check
generated="$(mktemp -d)"
trap 'rm -rf "$generated"' EXIT
buf generate --output "$generated"
diff -ru -x __pycache__ src/fanscore/optimizer/v1 "$generated/src/fanscore/optimizer/v1"
'';
lint.exec = ''
set -e
buf lint
buf format --diff --exit-code --path proto/fanscore/optimizer
uv run ruff check
uv run ruff format --check
'';
lint.exec = "uv run ruff check && uv run ruff format --check";
test.exec = "uv run pytest";
test-go.exec = "uv run python scripts/test_go.py";
health.exec = "uv run optimizer-health";
};

Expand All @@ -39,12 +47,12 @@
ruff = {
enable = true;
entry = "uv run ruff check";
excludes = [ "^src/optimizer/v1/[^/]+_pb2(\\.pyi?|_grpc\\.py)$" ];
excludes = [ "^src/fanscore/" ];
};
ruff-format = {
enable = true;
entry = "uv run ruff format --check";
excludes = [ "^src/optimizer/v1/[^/]+_pb2(\\.pyi?|_grpc\\.py)$" ];
excludes = [ "^src/fanscore/" ];
};
check-merge-conflicts.enable = true;
check-yaml.enable = true;
Expand Down
15 changes: 0 additions & 15 deletions gen/go/go.mod

This file was deleted.

18 changes: 0 additions & 18 deletions gen/go/go.sum

This file was deleted.

63 changes: 0 additions & 63 deletions gen/go/optimizer/v1/client_test.go

This file was deleted.

Loading
Loading