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 |
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:
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: 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eafeca22fe3067baafb0faec03448333e28fc4d8e67c44dbf6b998613e85d6c9
|
|
| MD5 |
0563f70050884acb0d01435f47da86b8
|
|
| BLAKE2b-256 |
c69686a3c3ec665f28ba8077bbc79ee25935a8f2705445036b946f18c5d604f0
|
Provenance
The following attestation bundles were made for tiny_pacs-0.3.0.tar.gz:
Publisher:
publish.yml on blanebf/tiny_pacs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tiny_pacs-0.3.0.tar.gz -
Subject digest:
eafeca22fe3067baafb0faec03448333e28fc4d8e67c44dbf6b998613e85d6c9 - Sigstore transparency entry: 2679552353
- Sigstore integration time:
-
Permalink:
blanebf/tiny_pacs@ac04410cae92b37e0190e038849c953b895ecedd -
Branch / Tag:
refs/heads/master - Owner: https://github.com/blanebf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ac04410cae92b37e0190e038849c953b895ecedd -
Trigger Event:
workflow_dispatch
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d8ef0babc71d34bd74efac9f502ff5936f9559ff71bdc170ae67986b8dde63a
|
|
| MD5 |
52e027c1a33755c4ab750a0066151e7b
|
|
| BLAKE2b-256 |
5f0af637df13cab675bf6bd85463938ffb8b5b2ee71d08fa1547d3404d912d40
|
Provenance
The following attestation bundles were made for tiny_pacs-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on blanebf/tiny_pacs
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tiny_pacs-0.3.0-py3-none-any.whl -
Subject digest:
2d8ef0babc71d34bd74efac9f502ff5936f9559ff71bdc170ae67986b8dde63a - Sigstore transparency entry: 2679552466
- Sigstore integration time:
-
Permalink:
blanebf/tiny_pacs@ac04410cae92b37e0190e038849c953b895ecedd -
Branch / Tag:
refs/heads/master - Owner: https://github.com/blanebf
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ac04410cae92b37e0190e038849c953b895ecedd -
Trigger Event:
workflow_dispatch
-
Statement type: