Skip to content
Open
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
35 changes: 35 additions & 0 deletions .github/workflows/lint_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Lint archivist

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
lint:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Install dependencies
run: uv sync --python 3.11 --group dev

- name: Run lint
run: |
uv run ruff check

- name: Run lint
run: |
uv run ruff format --check --diff

- name: Run isort
run: |
uv run isort . --check --diff
24 changes: 24 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Release archivist

on:
release:
types: [published]

jobs:
deploy:
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Build package
run: uv build --python 3.11

- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
36 changes: 36 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Test archivist

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Install dependencies
run: uv sync --python ${{ matrix.python-version }} --group dev

- name: Run tests
run: |
uv run pytest

- name: Upload coverage report
if: matrix.python-version == '3.11'
run: uv run coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Archivist

[![Coverage Status](https://coveralls.io/repos/github/simonsobs/archivist/badge.svg?branch=main)](https://coveralls.io/github/simonsobs/archivist?branch=main)

Tools for offline data storage compatible with the Librarian.
14 changes: 12 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ name = "archivist"
dynamic = ["version"]
description = "Archival data storage for the Librarian"
readme = "README.md"
requires-python = ">=3.9"
requires-python = ">=3.11"
license = "MIT"
license-files = ["LICENSE"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
Expand All @@ -36,6 +37,8 @@ write_to = "src/archivist/_version.py"
dev = [
"coverage>=7.10.7",
"coveralls>=4.0.1",
"httpx>=0.28.1",
"hypothesis>=6.141.1",
"isort>=6.1.0",
"pre-commit>=4.3.0",
"pytest>=8.4.2",
Expand Down Expand Up @@ -113,3 +116,10 @@ mark-parentheses = false
profile = "black"
line_length = 120
skip = ["_version.py"]

[tool.pytest.ini_options]
addopts = "--random-order --cov=archivist --cov-report=term-missing --cov-report=xml"

[tool.coverage.run]
source = ["archivist"]
branch = true
137 changes: 137 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Copyright (c) 2025-2026 Simons Observatory.
# Full license can be found in the top level "LICENSE" file.
"""Shared pytest fixtures for the archivist test suite."""

from datetime import datetime, timezone

import pytest

import archivist.database as database
import archivist.queue as queue_module
import archivist.settings as settings_module
from archivist.settings import Settings
from archivist.storage.storage_disk import StorageDisk


@pytest.fixture(autouse=True)
def _reset_module_singletons():
"""
Several modules cache lazily-created global state (the DB engine,
session maker, loaded settings, the in-memory status queue, and the
shared StorageDisk thread pool). Reset all of it before and after
every test so tests don't leak state into one another.
"""

def _reset():
database._engine = None
database._SessionMaker = None
settings_module._settings = None
queue_module._status_queue = None
StorageDisk._executor = None

_reset()
yield
_reset()


@pytest.fixture
def archive_root(tmp_path):
path = tmp_path / "archive_root"
path.mkdir()
return path


@pytest.fixture
def local_root(tmp_path):
path = tmp_path / "local_root"
path.mkdir()
return path


@pytest.fixture
def settings(tmp_path, archive_root, local_root) -> Settings:
"""A Settings instance backed by a throwaway on-disk sqlite database."""

db_path = tmp_path / "archivist_test.db"
return Settings(
database_driver="sqlite",
database=str(db_path),
archive_type="posix",
archive_root=str(archive_root),
local_root=str(local_root),
)


@pytest.fixture
def use_settings(monkeypatch, settings, tmp_path):
"""
Make `get_settings()` return `settings`, regardless of whether the
caller does `import archivist.settings` or
`from archivist.settings import get_settings` (the latter binds its
own reference, so patching the module attribute alone would miss it).
We instead drive this through the same env-var + cache mechanism
`get_settings()` itself uses.
"""

config_path = tmp_path / "test_config.json"
config_path.write_text(settings.model_dump_json())

monkeypatch.setenv("ARCHIVIST_CONFIG_PATH", str(config_path))
monkeypatch.setattr(settings_module, "_settings", None)

return settings_module.get_settings()


@pytest.fixture
def db_session(use_settings):
"""A real database session against a freshly created schema."""

database.create_all()
session = database.get_session()
try:
yield session
finally:
session.close()


def make_manifest_entry(**overrides):
"""Build a dict-form manifest entry, suitable for `ManifestEntry(**entry)`."""

now = datetime(2026, 1, 1, tzinfo=timezone.utc)
entry = dict(
name="file.txt",
create_time=now,
size=1024,
checksum="0" * 64,
uploader="test-uploader",
source="/source",
instance_path="/source/file.txt",
instance_create_time=now,
instance_available=True,
outgoing_transfer_id=0,
)
entry.update(overrides)
return entry


def make_manifest_entry_json(**overrides):
"""Like `make_manifest_entry`, but with datetimes as ISO strings for use as raw HTTP JSON bodies."""

entry = make_manifest_entry(**overrides)
for key in ("create_time", "instance_create_time"):
if isinstance(entry[key], datetime):
entry[key] = entry[key].isoformat()
return entry


@pytest.fixture
def manifest_entry_data():
return make_manifest_entry()


@pytest.fixture
def manifest_request_data(manifest_entry_data):
return {
"librarian_name": "test-librarian",
"store_files": [manifest_entry_data],
}
Loading