Skip to main content

tiny_pacs

CI

tiny_pacs is a small, pure-Python PACS (Picture Archiving and Communication System). It implements a DICOM Service Class Provider (SCP) for the most common services:

  • Verification (C-ECHO)

  • Storage (C-STORE)

  • Query/Retrieve: C-FIND, C-MOVE and C-GET at the PATIENT, STUDY, SERIES and IMAGE levels

  • Storage Commitment

Instead of a single monolith, the server is composed of components that talk to each other through a trolleybus event bus. DICOM networking is provided by pynetdicom2, dataset handling by pydicom and persistence by peewee.

Features

  • Modular design: database, device registry, PACS logic and storage are individual components that can be configured, replaced or extended independently

  • SQLite (file or in-memory, the default) and PostgreSQL backends with connection pooling

  • Storage backends: on-disk files, in-memory datasets or temporary files

  • TLS for incoming DICOM connections

  • DICOM user identity negotiation: credentials are presented on outgoing connections; incoming associations are authenticated per device by the optional tiny-pacs-identity extension

  • Interactive configuration wizard

  • YAML or JSON configuration files

  • Configuration is described with pydantic models and validated at load time; each component supplies its own config model

  • Extensible through Python entry points: optional extension packages add components and tiny-pacs CLI subcommands without core changes

Requirements

Python >= 3.10. Runtime dependencies: pydicom 3.x, pynetdicom2 0.9.x, peewee 4.x, PyYAML 6.x, trolleybus 0.2 and pydantic 2.x.

For the PostgreSQL backend install a driver separately, e.g. pip install psycopg2-binary.

Installation

From the git repository:

pip install git+https://github.com/blanebf/tiny_pacs.git

For development, with Poetry:

git clone git@github.com:blanebf/tiny_pacs.git
cd tiny_pacs
poetry install

Optional extensions

Extensions are optional packages that plug into tiny_pacs through entry points. Every distribution is independently optional; identity builds on admin, so installing it pulls the admin extension in automatically. See the documentation for details.

Feature matrix

Feature

tiny_pacs (core)

tiny-pacs-admin

tiny-pacs-identity

DICOM SCP (C-ECHO, C-STORE, C-FIND/C-MOVE/C-GET, commitment)

yes

Components: Database, Devices, PACS, storage backends

yes

run / config CLI + interactive wizard

yes

Entry-point plugin API (components + CLI subcommands)

yes

DeviceStore: DB-backed device registry with a per-device identity policy and configurable auto-add defaults

yes

devices / components / db offline admin CLI

yes

Users: user accounts with salted password hashes in the DB

yes

UserIdentityAuth: association authentication driven by the calling device’s identity policy

yes

users CLI (offline account management)

yes

Install matrix — the convenience extras pull the extension packages in, or install an extension directly with the same effect:

Command

Installs

pip install tiny_pacs

the core server only

pip install tiny_pacs[admin]

core + tiny-pacs-admin

pip install tiny_pacs[identity]

core + tiny-pacs-identity (pulls in tiny-pacs-admin)

pip install tiny_pacs[admin,identity]

core + both extensions

pip install tiny-pacs-admin / pip install tiny-pacs-identity

the given extension (and the core) directly

Installing an extension never changes server behaviour on its own: its components stay disabled until enabled in the configuration, and its CLI subcommands are additive.

Quick start

The CLI provides two built-in commands: run starts the server and config generates or inspects a configuration file (config show dumps the effective configuration). Running tiny-pacs without a command is equivalent to tiny-pacs run, so the traditional invocation keeps working. Extensions can add further subcommands.

A real deployment runs from its own configuration folder. Generate the configuration once — filled with the built-in defaults plus every component provided by installed extensions (the latter disabled) — together with the launcher scripts, cli.sh for POSIX shells and cli.cmd for Windows:

mkdir pacs && cd pacs
tiny-pacs config -o config.yaml --launcher

Edit config.yaml for the deployment: AE title and port, a file-backed database, on-disk storage instead of InMemoryStorage, TLS, extension components. Each launcher script changes into its folder and forwards every argument to tiny-pacs with -c config.yaml appended, so relative paths resolve and any subcommand runs without activating an environment:

./cli.sh run                # POSIX: start the server
./cli.sh config show        # dump the effective configuration
cli.cmd run                 # Windows

To pass configuration files yourself instead, use -c (YAML by extension, JSON for *.json); command-line options override what the loaded configuration sets, e.g. the AE title and/or the port:

tiny-pacs run -c config.yaml
tiny-pacs run -c config.yaml -a MY_PACS -p 4242

-c accepts several files; they are applied in order. tiny-pacs config without -o prints the generated YAML to stdout.

Without any configuration, run falls back to the bare built-in defaults — AE title TINY_PACS, port 11112, an in-memory SQLite database and in-memory storage. Enough for a smoke test; nothing survives a restart:

tiny-pacs run

Run either command in interactive mode: the wizard asks for every configuration value; with config the result is written to --output (or printed to stdout), with run it is merged into the server configuration and the wizard offers to save it to a file before starting the server:

tiny-pacs config -i -o config.yaml
tiny-pacs run -i

Each command describes its options in its help:

tiny-pacs run --help
tiny-pacs config --help

Configuration

A configuration file has three optional top-level sections: ae, log and components. Anything that is not provided falls back to the built-in defaults.

Configuration is described with pydantic models and validated when it is loaded, so unknown keys, wrong types or out-of-range values are rejected up front with a pydantic.ValidationError instead of surfacing as obscure errors at runtime:

  • ae → tiny_pacs.config.AEConfig (with an optional nested tiny_pacs.config.TLSConfig)

  • each entry under components → the config model the corresponding component provides through its config_model attribute (see Extending tiny_pacs)

Upgrading from the older dict-based configuration:

  • validation is strict — keys that are unknown to the models (in ae, tls or any component section) are rejected at load time instead of being silently ignored;

  • every component is skipped unless its entry sets on: true explicitly;

  • a component entry provided in a configuration source replaces any previous entry for that component wholesale — omitted fields fall back to the model defaults;

  • tiny_pacs.config.Config is a pydantic model, not a dict: use conf.ae, conf.log, conf.components and conf.model_dump();

  • device entries are tiny_pacs.devices.DeviceConfig models — e.g. DICOMClient.remote_ae.username instead of remote_ae['username'];

  • custom components must declare their config model (see Extending tiny_pacs) and be registered via tiny_pacs.config.register_component.

ae

Application entity settings:

  • ae_title — a list of AE titles the server answers to (default: ['TINY_PACS'])

  • port — the SCP TCP port (default: 11112)

  • max_pdu_length — maximum PDU length in bytes (default: 65536)

  • dump_ds — dump datasets and PDUs to the log (default: true)

  • supported_ts — a list of supported transfer syntax UIDs (default: the common uncompressed, JPEG, JPEG-LS, JPEG 2000, MPEG and RLE syntaxes)

  • tls — when present, all incoming connections are wrapped in TLS. A mapping with the certificate and, if the key is stored separately, the key entry pointing to PEM files, and an optional ca entry. When ca is given, client certificates are verified against it:

ae:
  tls:
    certificate: /etc/tiny_pacs/server.pem
    key: /etc/tiny_pacs/server.key
    ca: /etc/tiny_pacs/ca.pem

log

A standard Python logging.config.dictConfig schema. The default configuration installs a console handler with DEBUG level:

log:
  version: 1
  formatters:
    simple:
      format: '%(asctime)s - %(levelname)-8s - %(name)-15s - %(message)s'
  handlers:
    console:
      class: logging.StreamHandler
      level: DEBUG
      formatter: simple
      stream: ext://sys.stdout
  root:
    level: DEBUG
    handlers: [console]

components

A mapping of component name (see the component registry) to its configuration. Every component accepts an on flag and is skipped unless it is set to true. When no components are configured at all, the default set is used: Database, Devices, PACS and InMemoryStorage.

Example

ae:
  ae_title: [TINY_PACS]
  port: 11112
  max_pdu_length: 65536
  dump_ds: false
log:
  version: 1
  formatters:
    simple:
      format: '%(asctime)s - %(levelname)-8s - %(name)-15s - %(message)s'
  handlers:
    console:
      class: logging.StreamHandler
      level: INFO
      formatter: simple
      stream: ext://sys.stdout
  root:
    level: INFO
    handlers: [console]
components:
  Database:
    on: true
    driver: sqlite            # or postgres
    db_name: pacs.db          # SQLite database name, default 'pacs.db'
    mode: rwc                 # 'rwc' persists to the db_name file; the
                              # default 'memory' keeps data in RAM only
  Devices:
    on: true
    auto_add: true            # register calling AE titles automatically
    default_port: 11114       # port used for auto-added devices
    devices:
      WORKSTATION:
        aet: WORKSTATION
        address: 192.168.1.10
        port: 11114           # not 11113: keep clear of the shared
                              # HTTP server default port
        # Optional DICOM user identity for outgoing connections to this
        # device (used for C-MOVE sub-operations and Storage Commitment):
        # username: dicom_user
        # password: secret
  PACS:
    on: true
  FileStorage:
    on: true
    storage_dir: /var/lib/tiny_pacs/storage

The PostgreSQL driver takes connection parameters instead:

components:
  Database:
    on: true
    driver: postgres
    db_name: tiny_pacs_db
    host: localhost
    port: 5432
    user: postgres
    password: postgres
    max_conn: 20

Component registry

Component

Description

Database

Manages the database connection (SQLite by default, or PostgreSQL), transactions and table creation. On startup it collects the models of all components and creates their tables.

Devices

Registry of remote DICOM devices by AE title, used as C-MOVE destinations and for outgoing connections. When auto_add is enabled, calling AE titles are registered automatically with default_port.

PACS

The PACS services themselves: handling of C-STORE, C-FIND, C-MOVE, C-GET and Storage Commitment requests on top of the Patient/Study/Series/Instance database models.

HttpServer

The single HTTP server of the process (waitress, from the tiny_pacs[http] extra): dispatches requests to the WSGI applications contributed by HTTP front-ends (admin console, DICOMweb, …) by longest URL prefix. Binds nothing while no application is contributed.

FileStorage

Stores incoming datasets on disk: one file per SOP Instance in daily (YYYYMMDD) sub-folders of storage_dir. If no directory is configured, a temporary one is created and removed on shutdown.

InMemoryStorage

Keeps incoming datasets in RAM. Intended for testing.

TempFileStorage

Stores incoming datasets in temporary files and removes them on shutdown. Intended for testing.

Enable exactly one storage component — all storage components react to the same events, so enabling more than one stores every dataset multiple times.

Extending tiny_pacs

Every component provides its own configuration as a pydantic model, and the config loader validates the raw components section against the model each component declares. To add your own component, subclass tiny_pacs.component.Component and declare a tiny_pacs.component.ComponentConfig subclass through the config_model attribute:

from tiny_pacs import component, config

class MyConfig(component.ComponentConfig):
    # ``on`` (enable/disable) is provided by ComponentConfig
    some_option: str = 'default'

class MyComponent(component.Component[MyConfig]):
    config_model = MyConfig

    def on_start(self) -> None:
        super().on_start()
        # ``self.config`` is a validated ``MyConfig`` instance
        self.log_info('Got option %s', self.config.some_option)

Register the component so the loader knows which model to validate its configuration against, then enable it in your config file:

config.register_component('MyComponent', MyComponent)
components:
  MyComponent:
    on: true
    some_option: hello

Loading a configuration for MyComponent now validates some_option at load time, rejecting unknown keys or wrong types. Plain dicts handed directly to a component constructor are validated the same way, so both the file-based and the programmatic paths get the same guarantees.

Talking to tiny_pacs: command line examples

The examples below use the CLI shipped with pynetdicom2; DCMTK tools (echoscu, storescu, findscu, movescu) work just as well.

C-ECHO (verification):

python -m pynetdicom2 verify --local_aet CLIENT --aet TINY_PACS \
    --address localhost --port 11112

C-STORE:

python -m pynetdicom2 store --local_aet CLIENT --aet TINY_PACS \
    --address localhost --port 11112 --file_or_dir image.dcm

C-FIND (list patients whose name matches DOE*):

python -m pynetdicom2 find --local_aet CLIENT --aet TINY_PACS \
    --address localhost --port 11112 --level patient \
    --attr 'PatientName=DOE*' 'PatientID='

C-MOVE (retrieve one study):

python -m pynetdicom2 move --local_aet CLIENT --aet TINY_PACS \
    --address localhost --port 11112 --level study \
    --attr 'StudyInstanceUID=1.2.3.4'

The server knows C-MOVE destinations only through its Devices registry — either pre-configure the destination device, or rely on auto_add to register it on its first association. Note that in pynetdicom2 0.9.8 the CLI’s own move receiver (--local_port) does not accept storage associations, so use a real SCP (for example a second tiny_pacs instance) as the destination.

C-GET is supported on the server side as well and can be exercised with any C-GET capable service class user.

Python API

tiny_pacs ships a small DICOM client for programmatic access:

import pydicom
from tiny_pacs.client import DICOMClient

remote = {'aet': 'TINY_PACS', 'address': 'localhost', 'port': 11112}
client = DICOMClient('CLIENT', remote)

# C-ECHO
client.echo()

# C-STORE
client.store('image.dcm')

# C-FIND
query = pydicom.Dataset()
query.QueryRetrieveLevel = 'STUDY'
query.StudyDate = ''
for match in client.find(query):
    print(match.StudyInstanceUID)

# C-MOVE to a known destination
move = pydicom.Dataset()
move.QueryRetrieveLevel = 'STUDY'
move.StudyInstanceUID = '1.2.3.4'
client.move(move, dest_ae='WORKSTATION')

License

tiny_pacs is licensed under the MIT license, see the LICENSE file.

Metadata

Release files for tiny-pacs 0.4.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 tiny-pacs 0.4.0
File Size Uploaded Provenance Actions
tiny_pacs-0.4.0.tar.gz 103.8 kB With provenance Details for tiny_pacs-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for tiny-pacs 0.4.0
File Interpreter ABI Platform Actions
tiny_pacs-0.4.0-py3-none-any.whl Python 3 none any Details for tiny_pacs-0.4.0-py3-none-any.whl

Total release size: 218.3 kB

Release files / tiny_pacs-0.4.0.tar.gz

Download URL tiny_pacs-0.4.0.tar.gz
Size 103.8 kB
Tags Source
SHA-256 checksum
How to use checksums
3f30ec6ffea8414cd6aebf54c10669ecc129a21dbefe1dba7561f18ecf0dd74e
BLAKE2b-256 checksum
How to use checksums
1d9256200e2281293fea2995e28109fe4496fb5ac1b52fbc5a85aeb7c1fc706c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release files / tiny_pacs-0.4.0-py3-none-any.whl

Download URL tiny_pacs-0.4.0-py3-none-any.whl
Size 114.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
45283b8db829a2c90e7682a15174b0be92dcbbc4ca7a3668921b93f9eaf3d1b3
BLAKE2b-256 checksum
How to use checksums
18e1c2587637cb5a49c7dab80262d4a8a0185c1027e4c96ad1f3380762c63525
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Security for tiny-pacs 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for tiny_pacs-0.4.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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