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
2 changes: 1 addition & 1 deletion .github/actions/docker-deploy/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ runs:
flags: |
--add-cloudsql-instances ${{ inputs.deployment-sql-instance }}
--vpc-connector ${{ inputs.deployment-vpc-connector }}
--vpc-egress all
--vpc-egress private-ranges-only
--allow-unauthenticated
--memory ${{ env.MEMORY }}
--cpu ${{ env.CPU }}
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/unit-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ runs:
shell: bash

- name: Install Dependencies
run: poetry install --with dev,test
run: poetry install --with dev,test,scripts
shell: bash

- name: Run Linting
Expand Down
174 changes: 174 additions & 0 deletions .github/workflows/build-and-deploy-workflow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
name: CAS Repository Build and Deploy Workflow
on:
workflow_dispatch:
inputs:
service-type:
description: 'Service type to build and deploy. Can be `compute`, `admin` or `both`. Default is `both`.'
required: false
default: 'both'
type: choice
options:
- both
- compute
- admin
image-tag:
description: 'Docker image tag. Will use the short hash of the last commit in the branch if nothing is passed in.'
required: false
default: ''
add-latest-tag:
description: 'Push a `latest` tag to the artifact registry. Default is `true`.'
required: false
default: true
type: boolean
deployment-prefix:
description: 'The prefix to add to the deployment names'
required: true
default: ''
service-account-email:
description: 'Service account email used as the identity for the deployment.'
required: true
default: ''
sql-instance:
description: 'The SQL instance to connect to in the form [project]:[region]:[instance]'
required: true
default: ''
vpc-connector:
description: 'The VPC connector to use for the deployment in the form projects/[project]/locations/[region]/connectors/[connector]'
required: true
default: ''
config-secret:
description: 'The Google secret to use for the deployment configuration in the form [secret-name]:[version (or "latest")]'
required: true
default: ''
region:
description: 'The region to deploy the image to'
required: false
default: 'us-central1'
flavor:
description: 'The nature of the deployment set. Configurations are set based on this value and the values can be configured in the deploy/cloudrun/{flavor}.json files.'
required: true
default: 'default'

jobs:

prepare:
name: Resolve Image Tag
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.get-tag.outputs.tag }}
steps:
- uses: actions/checkout@v4

- id: get-tag
name: Get image tag
env:
IMAGE_TAG: ${{ inputs.image-tag }}
run: |
if [[ "$IMAGE_TAG" == "" ]]; then
echo "tag=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
else
echo "tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
fi
shell: bash

build-images:
name: Build Docker Images
needs: prepare
runs-on: ubuntu-latest

permissions:
contents: write
id-token: write

strategy:
matrix:
service-type: [ 'compute', 'admin' ]

env:
DOCKER_REGISTRY_NAME: us-central1-docker.pkg.dev
COMPUTE_IMAGE_NAME: us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/cas-services-cicd/cas-compute
ADMIN_IMAGE_NAME: us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/cas-services-cicd/cas-admin

steps:
- uses: actions/checkout@v4

- id: build-compute
uses: ./.github/actions/docker-build
if: matrix.service-type == 'compute' && (inputs.service-type == 'compute' || inputs.service-type == 'both')
with:
docker-registry-name: ${{ env.DOCKER_REGISTRY_NAME }}
image-name: ${{ env.COMPUTE_IMAGE_NAME }}
image-tag: ${{ needs.prepare.outputs.image-tag }}
service-type: ${{ matrix.service-type }}
gcp-provider-id: ${{ secrets.GCP_PROVIDER_ID }}
gcp-service-account-email: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
add-latest-tag: ${{ inputs.add-latest-tag }}

- id: build-admin
uses: ./.github/actions/docker-build
if: matrix.service-type == 'admin' && (inputs.service-type == 'admin' || inputs.service-type == 'both')
with:
docker-registry-name: ${{ env.DOCKER_REGISTRY_NAME }}
image-name: ${{ env.ADMIN_IMAGE_NAME }}
image-tag: ${{ needs.prepare.outputs.image-tag }}
service-type: ${{ matrix.service-type }}
gcp-provider-id: ${{ secrets.GCP_PROVIDER_ID }}
gcp-service-account-email: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
add-latest-tag: ${{ inputs.add-latest-tag }}

deploy-services:
name: Deploy CAS Services
needs: [ prepare, build-images ]
runs-on: ubuntu-latest

permissions:
contents: write
id-token: write

env:
DOCKER_REGISTRY_NAME: us-central1-docker.pkg.dev
COMPUTE_IMAGE_NAME: us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/cas-services-cicd/cas-compute
ADMIN_IMAGE_NAME: us-central1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/cas-services-cicd/cas-admin

steps:
- uses: actions/checkout@v4

- id: deploy-admin
name: Deploy the CAS Admin Service
uses: ./.github/actions/docker-deploy
if: inputs.service-type == 'admin' || inputs.service-type == 'both'
with:
docker-registry-name: ${{ env.DOCKER_REGISTRY_NAME }}
image-name: ${{ env.ADMIN_IMAGE_NAME }}
image-tag: ${{ needs.prepare.outputs.image-tag }}
gcp-provider-id: ${{ secrets.GCP_PROVIDER_ID }}
gcp-service-account-email: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
deployment-type: cas-admin
deployment-prefix: ${{ inputs.deployment-prefix }}
deployment-project: ${{ secrets.GCP_PROJECT_ID }}
deployment-service-account-email: ${{ inputs.service-account-email }}
deployment-sql-instance: ${{ inputs.sql-instance }}
deployment-vpc-connector: ${{ inputs.vpc-connector }}
deployment-config-secret: ${{ inputs.config-secret }}
deployment-region: ${{ inputs.region }}
deployment-flavor: ${{ inputs.flavor }}

- id: deploy-compute
name: Deploy the CAS Compute Service
uses: ./.github/actions/docker-deploy
if: inputs.service-type == 'compute' || inputs.service-type == 'both'
with:
docker-registry-name: ${{ env.DOCKER_REGISTRY_NAME }}
image-name: ${{ env.COMPUTE_IMAGE_NAME }}
image-tag: ${{ needs.prepare.outputs.image-tag }}
gcp-provider-id: ${{ secrets.GCP_PROVIDER_ID }}
gcp-service-account-email: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }}
deployment-type: cas-compute
deployment-prefix: ${{ inputs.deployment-prefix }}
deployment-project: ${{ secrets.GCP_PROJECT_ID }}
deployment-service-account-email: ${{ inputs.service-account-email }}
deployment-sql-instance: ${{ inputs.sql-instance }}
deployment-vpc-connector: ${{ inputs.vpc-connector }}
deployment-config-secret: ${{ inputs.config-secret }}
deployment-region: ${{ inputs.region }}
deployment-flavor: ${{ inputs.flavor }}
6 changes: 4 additions & 2 deletions .github/workflows/deploy-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@ jobs:

- id: get-image-tag
name: Get tag
env:
IMAGE_TAG: ${{ inputs.image-tag }}
run: |
if [[ "${{ inputs.image-tag }}" == "" ]]; then
if [[ "$IMAGE_TAG" == "" ]]; then
echo "IMAGE_TAG=latest" >> $GITHUB_ENV
else
echo "IMAGE_TAG=${{ inputs.image-tag }}" >> $GITHUB_ENV
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
fi
shell: bash
- id: deploy-admin
Expand Down
66 changes: 66 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,72 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`
and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_.


1.8.4 - 2026-04-29
------------------

Added
~~~~~
- Added API endpoint for serving precomputed Cell Ontology (CL) resources (#214)
- Added ``OntologicalColumnInfo`` model and API response fields for ontology column metadata (#214)
- Added script to generate and upload CL ontology resources (#214)
- Added unified build-and-deploy GitHub Actions workflow for CAS services
- Increased Uvicorn worker count and Cloud Run concurrency settings

Fixed
~~~~~
- Fixed VPC egress setting to ``private-ranges-only`` in deploy action
- Fixed image tag handling in build and deploy workflows

Changed
~~~~~~~
- Raised the minimum supported client version to ``1.8.0``
- Updated dependencies: unpinned ``numpy`` (now resolved transitively), bumped ``torch`` lower bound to ``>=2.4.0``
- Temporarily downgraded ``torch`` and ``cellarium-ml`` for compatibility, then re-pinned to stable versions


1.8.3 - 2026-04-15
------------------

Added
~~~~~
- Added ``memory_budget`` configuration option for the TileDB vector index

Fixed
~~~~~
- Fixed incorrect batch key handling in model inference (#212, #213)

Changed
~~~~~~~
- Updated Cloud Run deployment configuration


1.8.2 - 2026-04-07
------------------

Added
~~~~~
- Added TileDB Vector Search integration as the vector search backend (#209)
- Added support for querying cell metadata directly from the SOMA table
- Added support for multiple ontologies in cell annotation requests (#211)

Removed
~~~~~~~
- Removed Vertex AI Matching Engine client and all related vector search code (#209)

Changed
~~~~~~~
- Updated minimum required client version to 1.7.5
- Refactored vector search layer with a factory pattern and type aliases


1.8.1 - 2026-03-20
------------------

Fixed
~~~~~
- Fixed Read the Docs configuration for Python 3.12 (#207)


1.8.0 - 2026-03-20
------------------

Expand Down
6 changes: 1 addition & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ help:

# Export requirements.txt.lock for production (Docker, etc.)
requirements:
poetry export -f requirements.txt --output deploy/requirements.txt.lock --without-hashes --without dev,test,docs

# Export requirements.txt.lock for Docker builds
docker-requirements:
poetry export -f requirements.txt --output deploy/requirements.txt.lock --without-hashes --without dev,docs
poetry export -f requirements.txt --output deploy/requirements.txt.lock --without-hashes --without dev,test,docs,scripts

# Install Poetry
setup:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import typing as t

from fastapi import APIRouter, Depends
from fastapi.responses import RedirectResponse
from fastapi.responses import JSONResponse, RedirectResponse

from cellarium.cas_backend.apps.compute import dependencies, schemas
from cellarium.cas_backend.apps.compute.services.cell_quota_service import CellQuotaService
Expand Down Expand Up @@ -147,3 +147,26 @@ async def increase_quota_for_user_by_email(
"""
user_for_increase = cellarium_general_service.get_user_by_email(user_email=user_email)
cell_quota_service.increase_quota(admin_user=admin_user, user_for_increase=user_for_increase)


@cellarium_general_router.get("/cell-ontology-resource/{name}", response_model=schemas.CellOntologyResourceResponse)
async def get_cell_ontology_resource(
name: str,
_: AuthUser,
cellarium_general_service: GeneralService,
):
"""
Get a precomputed cell ontology resource by its unique name.

Returns precomputed graph-derived fields so the client can skip building the ontology graph locally.

:param name: Unique name of the ontological column resource

:return: Precomputed cell ontology resource with cl_names, term-to-type mapping,
children dictionary, shortest and longest path lengths from root
"""
result = cellarium_general_service.get_cell_ontology_resource(name=name)
return JSONResponse(
content=result.model_dump(),
headers={"Cache-Control": "public, max-age=86400"},
)
1 change: 1 addition & 0 deletions cellarium/cas_backend/apps/compute/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from cellarium.cas_backend.apps.compute.schemas.cellarium_general import ( # noqa
ApplicationInfo,
CASModel,
CellOntologyResourceResponse,
ClientVersionInput,
ClientVersionOutput,
FeatureSchemaInfo,
Expand Down
22 changes: 21 additions & 1 deletion cellarium/cas_backend/apps/compute/schemas/cellarium_general.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import datetime

from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic import AliasPath, BaseModel, ConfigDict, Field, field_validator


class OntologicalColumnInfo(BaseModel):
model_config = ConfigDict(from_attributes=True)

ontology_resource_name: str
column_name: str
description: str | None = None


class CASModel(BaseModel):
Expand All @@ -11,6 +19,10 @@ class CASModel(BaseModel):
schema_name: str = Field(example="refdata-gex-mm10-2020-A")
is_default_model: bool = Field(example=False)
embedding_dimension: int = Field(example=512)
ontological_columns: list[OntologicalColumnInfo] = Field(
default_factory=list,
validation_alias=AliasPath("cell_info_metadata", "ontological_columns"),
)

# Set the default value for description if it is provided as None
@field_validator("description", mode="before")
Expand Down Expand Up @@ -51,3 +63,11 @@ class ClientVersionInput(BaseModel):
class ClientVersionOutput(BaseModel):
is_valid: bool
min_version: str


class CellOntologyResourceResponse(BaseModel):
cl_names: list[str]
cell_ontology_term_id_to_cell_type: dict[str, str]
children_dictionary: dict[str, list[str]]
shortest_path_lengths_from_cell_root: dict[str, int]
longest_path_lengths_from_cell_root: dict[str, int]
Loading
Loading