Skip to main content

arkitekt-service

What a service of an Arkitekt hub is made with. Its parts, each for one thing a service does:

arkitekt_service.contract What the service's image answers the hub's installer: what it needs, its own config written from the hub's facts, its migrations and upgrades.
arkitekt_service.service What the service is to the hub's rekuest: the structures it hosts and the signals it emits.
arkitekt_service.hook What can be done in its process: a HookAgent, whose actions rekuest reaches over HTTP.
arkitekt_service.server What every service is as a Django server: the settings blocks they all spell alike, ensureadmin, validate_settings.
arkitekt_service.testing A pytest fixture and check that prove an image in a hub made for the test.

They share arkitekt_service.trust — no secrets between services: every instance signs with its own key and the hub vouches for the public halves — and nothing else. A process uses any of them without the others.

pip install arkitekt-service

The contract

Everything that is done with a service's image goes through one command in it:

arkitekt-service                      # what it is, needs and offers, and what to run in it (JSON)
arkitekt-service serve                # become the service: serve, and nothing else
arkitekt-service debug                # the same, with the development server
arkitekt-service run migrate          # bring its database to this release, then its setup
arkitekt-service run <job>            # any other job it offers: plan, superuser, upgrade, its own
arkitekt-service standalone           # run migrate, then serve: one image, run on its own
arkitekt-service render               # this release's config, from the hub's facts
arkitekt-service check                # does this release read a config as written?

An installer (Konstruktor) then needs to know the hub, and nothing about a service that the service's image does not say. How a release spells its config, which keys it renamed, how it is started, what it has to do to its data — all of that ships in the image, with the code it belongs to. Exit code 78 is a release's own refusal (facts it cannot be configured from, a setting it does not read, a job it does not offer), with the reason on stderr.

The image says what it is

The image's own command is the first of these:

CMD ["arkitekt-service", "describe"]

So an installer asks by running the image with no command, and assumes nothing of what is inside: every other command it runs is one the answer named. A service written in another language takes part by printing the same description as its own command. Whoever starts the image by hand gets the description too, and beside it, on stderr, a note saying that nothing is being served and what to run instead.

What the description says:

  • name: what a hub calls the service: lowercase letters, digits and underscores, no hyphen.
  • identifier: what the service is registered as, and what a client asks for.
  • needs, offers, requires: what a hub has to provide, and what other services are wired to.
  • needs.databases: the databases it wants in the hub's Postgres, by name (main unless it says otherwise). A hub calls each <service>_<name> and hands it over under the name the service gave: blocks.postgres(facts, "events"). A name is one Postgres takes unquoted (a lowercase letter, then lowercase letters, digits and underscores); anything else is refused when the service is declared.
  • serve and debug: what a container of the image runs to serve, in production and in development. An installer writes the one that applies as the service's command.
  • jobs: what can be run in the image beside its start, by name, each with the command that runs it. migrate is every service's, and lists the jobs it runs as its setup (includes). An operator runs any of them again by name, e.g. konstruktor job run mikro ensureadmin, which is arkitekt-service run ensureadmin in a container of the image.
  • prepare: which job brings the database to the release (migrate).
  • render: what writes the release's config from the hub's facts.
  • hosts: the structures the service holds and the signals it sends about them, as data (see below). A hub's installer hands this to the hub's rekuest.
  • source: where the image's code came from (repository, commit) and where it sits in the image, so that an installer can run the service from a checkout. The build says it: ARKITEKT_SOURCE_REPOSITORY and ARKITEKT_SOURCE_REVISION in the image's environment.
  • sidecars: what runs beside the service as an image of its own: one it does not run without (rekuest's takt, named from the service's image), or an optional one it drives on a hub that has the use for it (Lok's mesh control server).

A container serves; a job prepares

A service's container does one thing: serve. arkitekt-service serve becomes the server and does nothing before that. Everything its database needs first is run migrate: wait for the database, apply the release's migrations, then run the jobs the service named as its setup (an admin account, seeded rows), all in one process. Whoever starts the service runs it as a job of its own, once per build, before the first start and before an update's:

docker compose run --rm --no-deps mikro arkitekt-service run migrate

or, in a compose file that has no installer, as a service the server waits for:

mikro-migrate:
  image: jhnnsrs/mikro:7
  command: arkitekt-service run migrate
mikro:
  image: jhnnsrs/mikro:7
  command: arkitekt-service serve
  depends_on:
    mikro-migrate:
      condition: service_completed_successfully

A container that merely restarts then does none of it.

arkitekt-service standalone is the two together, for the one case where that is what is wanted: a single image run on its own — a developer's compose file, a quick look at a release. It prepares, then serves, and serves nothing if preparing failed. Nothing that manages a stack uses it.

Declaring a service

A service declares itself in one module, named by ARKITEKT_SERVICE in its Dockerfile:

from arkitekt_service.contract import JSON, Contract, Description, Facts, Job, Needs, Offers, Start, blocks


def render(facts: Facts) -> dict[str, JSON]:
    document = blocks.server(facts)  # django, postgres, redis, authentikate
    document["datalayer"] = blocks.datalayer(facts, "media", "zarr")
    return document


contract = Contract(
    description=Description(name="mikro", identifier="live.arkitekt.mikro", needs=Needs(storage=["media", "zarr"])),
    settings=Settings,
    render=render,
    # How it is started. There is no script beside this: `arkitekt-service serve` becomes it.
    serve=Start(("daphne", "-b", "0.0.0.0", "-p", "80", "mikro_server.asgi:application")),
    debug=Start(("python", "manage.py", "runserver", "0.0.0.0:80")),
    # What else can be run in the image, by name, and which of it `migrate` runs as setup.
    jobs={"ensureadmin": Job(("ensureadmin",), "Create the operator account the config names")},
    setup=("ensureadmin",),
)

The hub's facts (arkitekt_service.contract.facts) and a service's description (arkitekt_service.contract.description) are versioned documents; an image refuses facts it does not understand rather than dropping them.

What a service hosts

Said once, as data, in the contract — so a hub knows it from the image, before the service runs:

from arkitekt_service.contract import Descriptor, Hosts, Signal, Structure

HOSTS = Hosts(
    structures=[Structure(identifier="@mikro/arraydataset", label="Array Dataset",
                          descriptors=[Descriptor(key="@mikro/n_channels", type="INT")])],
    signals=[Signal(identifier="@mikro/arraydataset", kinds=["CREATED", "UPDATED", "DELETED"],
                    descriptors=["@mikro/n_channels"])],
)
contract = Contract(description=Description(name="mikro", ..., hosts=HOSTS), ...)

and bound to the models and the code where Django is loaded:

from arkitekt_service.service import Service, organization_of
from arkitekt_service.hook import HookAgent

service = Service("mikro", hosts=contract.description.hosts)
dataset = service.structure(ArrayDataset, "@mikro/arraydataset", describe=array_descriptors)
service.model_signal(dataset, organization=organization_of())

agent = HookAgent("mikro", description="mikro's housekeeping")


@agent.action
def reembed_stale(organization: str) -> dict:
    """Re-embed stale rows."""
    ...


urlpatterns = [..., *service.urls, *agent.urls]

A structure the contract does not declare cannot be bound, and a service whose contract declares one that nothing binds does not start: what the image says and what the service holds cannot drift apart. The hub's rekuest catalogues what the installer handed it; the service still answers the same at _rekuest/service/manifest, for a rekuest nobody told.

A service says what exists; an agent says what can be done. Neither knows the other.

A Django server like the others

INSTALLED_APPS = [..., "arkitekt_service.server"]
from arkitekt_service.server.settings import DjangoSettings, PostgresSettings, RedisSettings, ServiceSettings


class Settings(ServiceSettings):  # the config file, with the environment over it
    django: DjangoSettings
    postgres: PostgresSettings
    redis: RedisSettings
    ...  # and what is the service's own

brings manage.py ensureadmin (the operator account the config names), manage.py validate_settings [--strict] (the config as the release reads it, secrets masked) and a check that warns, when migrate runs, about keys the release does not read.

Proving an image

# tests/conftest.py
pytest_plugins = ["arkitekt_service.testing"]


# tests/test_hub.py
@pytest.mark.hub
def test_the_image_runs_in_a_hub(service_hub):
    check_service(service_hub)

pytest -m hub --service-image mikro:dev starts a hub with nothing but a coordination server and that image, as an installer would, and asks of it what every service has to answer: ready, healthy, closed to nobody, open to an app the hub issued a token to, fully migrated, never restarted. It needs Docker and konstruktor; it is for the job that builds the image.

Development

uv sync
uv run pytest                                          # the contract and the service
uv run pytest tests/hook --ds=hook_project.settings    # the hook agent: a process that is no service
uv run basedpyright

Releases are cut from conventional commits on main (tag-only, like the other Arkitekt packages) and uploaded to PyPI.

Metadata

Release files for arkitekt-service 1.11.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for arkitekt-service 1.11.0
File Size Uploaded
arkitekt_service-1.11.0.tar.gz 47.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for arkitekt-service 1.11.0
File Interpreter ABI Platform
arkitekt_service-1.11.0-py3-none-any.whl Python 3 none any Details

Total release size: 107.6 kB

Release files / arkitekt_service-1.11.0.tar.gz

Download URL arkitekt_service-1.11.0.tar.gz
Size 47.4 kB
Tags Source
SHA-256 checksum
How to use checksums
43cdaf95db2009d575439a671087cb16e79d34f75c2cdab8484a762e08753655
BLAKE2b-256 checksum
How to use checksums
3e591ea3e606bab70b15dff8050a691873673bc49e81dbd76955ee12cb07cfed
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / arkitekt_service-1.11.0-py3-none-any.whl

Download URL arkitekt_service-1.11.0-py3-none-any.whl
Size 60.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c4eb98a9c4c28b0b7419418769bf7b7003e370a4c180d3cc3cccaa03c50fb091
BLAKE2b-256 checksum
How to use checksums
5cc448b33c48c79173beefac07e1bb4ad7caea0d000b9b9fa9a5d875843e2590
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

6.0.0

2 release files

5.2.1

2 release files

5.2.0

2 release files

5.1.0

2 release files

5.0.0

2 release files

4.0.0

2 release files

3.0.0

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

This release

1.11.0 This release

2 release files

1.9.0

2 release files

1.8.0

2 release files

1.7.0

2 release files

1.6.1

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page