Build a Gyoza Op#
A Gyoza Op is a self-contained, containerised operation that the Gyoza ecosystem can schedule and execute on any worker. This guide walks you through the project structure, the required configuration files, and the steps to build your op into a usable container image.
Prerequisites#
gyoza installed (see Installation).
Docker daemon running locally.
Basic familiarity with Pydantic models.
Project structure#
A minimal op project looks like this:
my-op/
├── main.py # Op logic with the @gyoza_op decorator
├── gyoza.yml # Gyoza manifest (image name, build config, …)
├── Dockerfile # Container recipe, installs deps and calls gyoza run
└── pyproject.toml # (or requirements.txt) Python dependencies
Each file has a clear responsibility:
main.py— defines the input model, the output model and the decorated function that performs the actual operation.gyoza.yml— tells gyoza how to build the Docker image and, later, how to deploy and run it.Dockerfile— standard Docker build recipe. It installs your dependencies and setsgyoza runas the container entrypoint.
The main.py file#
By default, the op’s entry point is main.py (configurable via the
gyoza_op_file key in gyoza.yml). This file contains the input model, the output model and the decorated function that performs the operation.
Inputs and outputs are defined as Pydantic models. Each model is a simple class that inherits from BaseModel and declares typed fields:
from pydantic import BaseModel, Field
class Input(BaseModel):
# Simple style — literal default (optional input)
top_k: int = 3
# Field style — description + examples appear on the definition Overview
image_path: str = Field(
description="Path to the input orthophoto",
examples=["s3://bucket/orto.tif"],
)
date: str = Field(
default="2024-01-15",
description="Reference date in YYYY-MM-DD format",
examples=["2024-01-15"],
)
class Output(BaseModel):
label: str = ""
confidence: float = Field(
description="Model confidence score between 0 and 1",
examples=[0.97],
)
Both styles are valid and can coexist in the same model. Use a literal
default (top_k: int = 3, label: str = "") when you only need an
optional value. Use Field(...) when you want a human-readable
description and/or examples so the definition Overview can show
them (Swagger-style): the UI prefers examples / example, and falls
back to default when no explicit example is set — useful for formats
like dates (YYYY-MM-DD vs MM/DD/YYYY).
Note
The optional description key in gyoza.yml describes the
product as a whole. Field(description=...) describes each
parameter. They are complementary.
Gyoza uses these models to validate the data that flows in and out of your
op at runtime. Fields with default values become optional inputs. Input
can also inherit from aanother class that the product already uses, the inherited
fields are published like the declared ones, and redeclaring one in Input
overrides it.
Specs are read from the source rather than by importing it, so defaults have
to be literals: numbers, strings, booleans, None, or lists, tuples and
dicts of those (constant arithmetic such as 1.0 / 10000.0 is folded). A
default_factory still marks the field optional, just without a published
default. Annotations become simplified types, so str | None is published
as string with required false, Literal["bap", "geomedian"] as
string, and list[str] as array[string].
The @gyoza_op decorator#
The @gyoza_op decorator ties everything together. It receives the input
and output models and wraps your function so gyoza can call it with
validated data:
from pydantic import BaseModel, Field
from gyoza import gyoza_op
class Input(BaseModel):
image_path: str = Field(
description="Path to the input image",
examples=["cat.jpg"],
)
top_k: int = 3
class Output(BaseModel):
label: str
confidence: float
@gyoza_op(input_model=Input, output_model=Output)
def classify_image(image_path: str, top_k: int = 3):
# ... load model, run inference ...
return "cat", 0.97
The function parameters must match the fields of input_model. The
return value is a tuple whose values are mapped positionally to the fields
of output_model. In the example above, "cat" maps to label and 0.97 maps to confidence.
Alternatively, the function can take the input model instance as its sole parameter, its handy when the input is a large configuration object and you don’t want to repeat every field in the signature:
@gyoza_op(input_model=Input, output_model=Output)
def classify_image(inputs: Input):
# ... use inputs.image_path, inputs.top_k ...
return "cat", 0.97
Both styles are detected automatically from the annotation. The return
value can also be a dict, a single value (for single-field output
models), or an instance of output_model itself.
When the container starts, gyoza run finds this decorated function,
feeds it the validated inputs, and serialises the output automatically.
File outputs#
Outputs are JSON, so large files should not be returned inline. Instead, declare an output field for the file and return its local path:
class Output(BaseModel):
mask: str # local path of the produced file
confidence: float
If the run was created with an upload destination for that field (see
Delivering file outputs), gyoza run uploads the file there
after your function returns and the final output holds the file’s URL.
Without a destination, the local path is returned as-is. Your op code
never handles storage or credentials either way.
Logging progress#
Use gyoza_logger inside your op to emit messages and report progress back
to the platform. Import it alongside gyoza_op and call it freely within
your function body:
from pydantic import BaseModel
from gyoza import gyoza_op, gyoza_logger
class Input(BaseModel):
image_path: str
top_k: int = 3
class Output(BaseModel):
label: str
confidence: float
@gyoza_op(input_model=Input, output_model=Output)
def classify_image(image_path: str, top_k: int = 3):
gyoza_logger.info("Loading model")
gyoza_logger.progress(25.0)
# ... load model ...
gyoza_logger.info("Running inference")
gyoza_logger.progress(75.0)
# ... run inference ...
gyoza_logger.progress(100.0)
return "cat", 0.97
The logger exposes three methods:
gyoza_logger.info(message)Emits a plain text log line. Use it for status messages, debug hints, or any information that helps trace what the op is doing.
gyoza_logger.progress(value, relative=False)Reports progress as a percentage between
0and100. Passrelative=Trueto increment from the current value instead of setting an absolute one. Progress can only move forward — passing a value lower than the current progress raises aValueError.gyoza_logger.reset()Resets the progress counter back to
0. Useful if your op runs multiple sub-steps that each go from 0 % to 100 %.
The gyoza.yml file#
The gyoza.yml file is the project manifest. It contains everything gyoza
needs to build, deploy, and run your op. For building, the two required keys are image and build.
name: classify-image
version: "1.0.0"
image: myregistry/classify-image:1.0.0
description: "Classifies an image and returns the top predicted label."
build:
context: .
dockerfile: Dockerfile
args:
GITHUB_TOKEN: "${GITHUB_TOKEN}"
USER_UID: "${DOCKER_USER_ID:-1000}"
GROUP_ID: "${GROUP_ID:-1000}"
imageThe full Docker image tag that will be produced by the build. Use your registry prefix so images are easy to identify and push later.
buildUnder the hood, gyoza generates a temporary
docker-compose.ymland copies this section directly into it, so it supports the same keys as a Compose build block. In general, the most common keys are:contextPath to the build context (usually
., the project root).dockerfilePath to the Dockerfile, relative to the context.
argsBuild-time variables injected as Docker
ARGvalues. Values support${ENV_VAR}and${ENV_VAR:-default}substitution, so secrets like tokens do not need to be committed to the manifest.
All the other keys supported by the Docker Compose build block are supported by gyoza.
See also
The remaining fields in gyoza.yml (e.g constraints, retry_policy,
deploy, event_delivery, etc.) are covered in
Deploy a Gyoza Op.
The Dockerfile#
The Dockerfile is a regular Docker build recipe. The only gyoza-specific
requirement is that the container’s default command must call
gyoza run, pointing it at your op file:
FROM python:3.12-slim
ARG GITHUB_TOKEN
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git build-essential \
&& rm -rf /var/lib/apt/lists/*
RUN git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
WORKDIR /app
COPY ./ /app/
# Install your project (and gyoza as a dependency)
RUN pip install --no-cache-dir .
CMD ["gyoza", "run", "--file", "main.py"]
In the above Dockerfile, we ensure that the gyoza run command is available inside the container by installing gyoza as a dependency and setting it as the container entrypoint, which calls the main.py file containing the op logic and IO models.
Build the image#
Once the three files are in place, build the gyoza op image with:
gyoza build ./my-op
On success, you will see:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Build completed successfully!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
After that you can check if the image was built successfully by running:
docker images
You should see the image you built in the list.
REPOSITORY ID
myregistry/sample-op 1.0.0 31f3e0f1cfec
See also
gyoza build is also the first step of gyoza deploy. Use it
standalone when you want to iterate on the image locally before doing a
full deploy cycle. For testing the image locally, you can check the Test a Gyoza Op guide.
Full example#
Below is a complete, minimal op project for reference.
main.py#
from pydantic import BaseModel
from gyoza import gyoza_op, gyoza_logger
class Input(BaseModel):
image_path: str
top_k: int = 3
class Output(BaseModel):
label: str
confidence: float
@gyoza_op(input_model=Input, output_model=Output)
def classify_image(image_path: str, top_k: int = 3):
gyoza_logger.info("Loading model")
gyoza_logger.progress(25.0)
# ... load model ...
gyoza_logger.info("Running inference")
gyoza_logger.progress(75.0)
# ... run inference ...
gyoza_logger.progress(100.0)
return "cat", 0.97
gyoza.yml#
name: sample-op
version: "1.0.0"
image: myregistry/sample-op:1.0.0
description: "Sample classification op used in the build guide."
build:
context: .
dockerfile: Dockerfile
args:
GITHUB_TOKEN: "${GITHUB_TOKEN}"
Dockerfile#
FROM python:3.12-slim
ARG GITHUB_TOKEN
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*
RUN git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
WORKDIR /app
COPY ./ /app/
RUN pip install --no-cache-dir .
CMD ["gyoza", "run", "--file", "main.py"]
pyproject.toml#
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "sample-op"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = [
"gyoza @ git+https://github.com/g-e-o-i-a/gyoza.git",
]
[tool.setuptools]
py-modules = ["main"]
Build and verify:
gyoza build ./sample-op
Next steps#
Deploy a Gyoza Op, push the image, register the OpDefinition on the server, and start running your op in the cluster.