Skip to main content
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.

Start the server with the built-in defaults — AE title TINY_PACS, port 11112, an in-memory SQLite database and in-memory storage:

tiny-pacs run

Override the AE title and/or the port from the command line:

tiny-pacs run -a MY_PACS -p 4242

Load configuration from a file (YAML by extension, JSON for *.json):

tiny-pacs run -c config.yaml

Generate a YAML configuration file filled with the default values — either print it to stdout or write it to a file:

tiny-pacs config
tiny-pacs config -o config.yaml

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:

  • aetiny_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: 11113       # port used for auto-added devices
    devices:
      WORKSTATION:
        aet: WORKSTATION
        address: 192.168.1.10
        port: 11113
        # 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.

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tiny_pacs-0.3.0.tar.gz (58.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tiny_pacs-0.3.0-py3-none-any.whl (66.1 kB view details)

Uploaded Python 3

File details

Details for the file tiny_pacs-0.3.0.tar.gz.

File metadata

  • Download URL: tiny_pacs-0.3.0.tar.gz
  • Upload date:
  • Size: 58.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tiny_pacs-0.3.0.tar.gz
Algorithm Hash digest
SHA256 eafeca22fe3067baafb0faec03448333e28fc4d8e67c44dbf6b998613e85d6c9
MD5 0563f70050884acb0d01435f47da86b8
BLAKE2b-256 c69686a3c3ec665f28ba8077bbc79ee25935a8f2705445036b946f18c5d604f0

See more details on using hashes here.

Provenance

The following attestation bundles were made for tiny_pacs-0.3.0.tar.gz:

Publisher: publish.yml on blanebf/tiny_pacs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tiny_pacs-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: tiny_pacs-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 66.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tiny_pacs-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2d8ef0babc71d34bd74efac9f502ff5936f9559ff71bdc170ae67986b8dde63a
MD5 52e027c1a33755c4ab750a0066151e7b
BLAKE2b-256 5f0af637df13cab675bf6bd85463938ffb8b5b2ee71d08fa1547d3404d912d40

See more details on using hashes here.

Provenance

The following attestation bundles were made for tiny_pacs-0.3.0-py3-none-any.whl:

Publisher: publish.yml on blanebf/tiny_pacs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 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