Skip to content

Commit 773e930

Browse files
Extract palace.util into a uv workspace package (PP-4076) (#3231)
## Description Convert the repo into a [`uv` workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/) and carve out a new namespace package `palace-util` (namespace `palace.util`) that sits at the bottom of the Palace dependency graph — other Palace packages depend on it; it depends on no other Palace package. The package holds the small set of reusable, dependency-light utilities: - `palace.util.exceptions` — the base of Palace exception hierarchy - `palace.util.datetime_helpers` — timezone-aware datetime helpers. - `palace.util.log` — the standardized logging toolkit ## Motivation and Context Sets up the monorepo as a `uv` workspace and extracts the minimum subset of palace-manager that needs to come along with the upcoming `palace-opds` extraction (#3230). Having `palace-util` as its own workspace member lets both `palace-manager` and `palace-opds` (and eventually any other Palace project) depend on it without any of them pulling in each other. JIRA: PP-4076 ## How Has This Been Tested? - Tests run in CI ## Checklist - [x] I have updated the documentation accordingly. - [x] All new and existing tests passed.
1 parent 5b11aa7 commit 773e930

348 files changed

Lines changed: 978 additions & 526 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ repos:
7676
- id: check-namespace-package
7777
name: Check that palace is a namespace package
7878
language: fail
79-
entry: Please remove src/palace/__init__.py
80-
files: ^src/palace/__init__.py$
79+
entry: Please remove src/palace/__init__.py - palace must remain a PEP 420 namespace.
80+
files: (^|/)src/palace/__init__\.py$
8181

8282
# Exclude test files, since they may be intentionally malformed
8383
exclude: ^tests/files/

CLAUDE.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ through mobile and web applications.
4949

5050
## Project Structure
5151

52-
- `/src/palace/manager` - Main application source code
52+
This repository is a [`uv` workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/). The main
53+
`palace-manager` application is at the repo root; reusable namespace packages live under `/packages`.
54+
55+
- `/src/palace/manager` - Main application source code (`palace-manager`)
5356
- `/api` - REST API endpoints for mobile applications
5457
- `/admin` - Administrative API endpoints for the web dashboard
5558
- `/celery` - Background worker processes and task definitions
@@ -64,12 +67,21 @@ through mobile and web applications.
6467
- `/service` - Dependency injection container and service layer
6568
- `/sqlalchemy` - Database models and schema definitions
6669
- `/util` - Reusable utility functions and helpers
67-
- `/tests` - Test files
70+
- `/packages` - Workspace member packages (each is independently buildable/publishable)
71+
- `/palace-util` - Shared utilities under the `palace.util` namespace: exceptions
72+
(`BasePalaceException`, `PalaceValueError`, `PalaceTypeError`), datetime helpers, and the full
73+
logging toolkit. Import from `palace.util.*` directly.
74+
- `/tests` - Test files for the whole repository (both `palace-manager` and workspace-member packages)
6875
- `/files` - Test fixture files
6976
- `/fixtures` - pytest fixtures shared across test functions
70-
- `/manager` - pytest test files (should mirror `src/palace/manager` structure)
77+
- `/manager` - pytest test files for `palace-manager` (should mirror `src/palace/manager` structure)
7178
- `/migration` - Database migration tests
7279
- `/mocks` - Test mocks (**being phased out - avoid in new code**)
80+
- `/palace_util` - pytest test files for the `palace-util` workspace package
81+
- Workspace-member tests live under the root `tests/<package_name>/` rather than
82+
`packages/<name>/tests/` so they share the repo's pytest fixtures and conftest plugins and
83+
tooling (mypy, coverage, `testpaths`) has a single tests tree to reason about. When adding a
84+
new workspace package, create its test tree as `tests/<package_name>/` alongside the others.
7385

7486
## Architecture Overview
7587

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ Palace Manager is a backend service for digital library systems, maintained by [
1515
- [Patron Blocking Rules — Allowed Functions](docs/FUNCTIONS.md)
1616
Reference for the functions available in patron blocking rule expressions.
1717

18+
## Repository Layout
19+
20+
This repository is a [`uv` workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/). The main
21+
`palace-manager` application lives at the repo root (`src/palace/manager/`). Reusable namespace packages
22+
live under `packages/`:
23+
24+
- [`palace-util`](packages/palace-util/README.md) — shared utilities (exceptions, datetime helpers,
25+
`LoggerMixin`) under the `palace.util` namespace.
26+
1827
## Installation
1928

2029
Docker images created from this code are available at:

docker/Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ COPY --chmod=644 docker/services/logrotate /etc/
1818
RUN mv /etc/cron.daily/logrotate /etc/cron.hourly/logrotate
1919

2020
# Copy our uv files into the image and install our dependencies.
21+
# We copy the workspace-member pyproject.toml files (but not their source) so that
22+
# uv can resolve workspace members without invalidating this layer on source changes.
2123
COPY --chown=palace:palace uv.lock pyproject.toml /var/www/circulation/
22-
RUN uv sync --frozen --no-dev --no-install-project
24+
COPY --chown=palace:palace packages/palace-util/pyproject.toml /var/www/circulation/packages/palace-util/
25+
RUN uv sync --frozen --no-dev --no-install-workspace
2326

2427
COPY --chown=palace:palace . /var/www/circulation
2528

docker/Dockerfile.baseimage

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,17 @@ RUN useradd -ms /bin/bash -U palace && \
6060

6161
WORKDIR /var/www/circulation
6262
COPY --chown=palace:palace uv.lock pyproject.toml /var/www/circulation/
63+
# Workspace-member pyproject.toml files are needed for uv sync to resolve the
64+
# workspace; we copy just the manifests (not the source) so this layer doesn't
65+
# get invalidated by source changes.
66+
COPY --chown=palace:palace packages/palace-util/pyproject.toml /var/www/circulation/packages/palace-util/
6367

6468
# Setup virtualenv and install our python dependencies.
6569
# What we install is based on the uv.lock file in the repo at the time this
6670
# image is built. These may get out of date, but we always rerun this step when
6771
# building the final image, so it will be up to date then. This gives is a base
6872
# to work from which speeds up the final image build.
69-
RUN uv sync --frozen --no-dev --no-install-project && \
73+
RUN uv sync --frozen --no-dev --no-install-workspace && \
7074
SIMPLIFIED_ENVIRONMENT=/var/www/circulation/environment.sh && \
7175
echo "if [ -f $SIMPLIFIED_ENVIRONMENT ]; then source $SIMPLIFIED_ENVIRONMENT; fi" >> env/bin/activate && \
7276
. env/bin/activate && \
@@ -76,5 +80,6 @@ RUN uv sync --frozen --no-dev --no-install-project && \
7680
rm -Rf /root/.cache && \
7781
rm pyproject.toml && \
7882
rm uv.lock && \
83+
rm -rf packages && \
7984
rm -rf /root/.cache && \
8085
/bd_build/cleanup.sh

packages/palace-util/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# palace-util
2+
3+
Small, dependency-light utilities shared across [Palace Project](https://thepalaceproject.org)
4+
Python packages. Lives in the `palace.util` namespace.
5+
6+
This package exists so that the handful of cross-cutting helpers that *every* Palace package needs
7+
— a common exception hierarchy, timezone-aware datetime handling, a standardized
8+
`LoggerMixin` — can be depended on without pulling in the heavier `palace-manager` application.
9+
Packages that need only these primitives depend on `palace-util` directly; `palace-manager` is
10+
also just a consumer.
11+
12+
## Scope
13+
14+
`palace-util` sits at the bottom of the Palace dependency graph: **other Palace packages depend
15+
on `palace-util`; `palace-util` depends on no other Palace package.**
16+
17+
If a utility is only useful inside the Palace Manager application, it does not belong here —
18+
keep it in `src/palace/manager/util/`. Candidates for `palace-util` should be:
19+
20+
- **Reusable** outside the manager application (e.g., by other Palace services, CLI tools, or
21+
by future extracted packages like `palace-opds`).
22+
- **Runtime-dependency-light** — stdlib-only is the ideal, and in the rare case a third-party
23+
library is justified it must be tiny and widely-used. Consumers of `palace-util` should be
24+
able to add it to their `pyproject.toml` without materially growing their install footprint.
25+
- **No intra-Palace dependencies**.
26+
- **Stable** enough that the wider ecosystem can rely on it without expecting frequent breaking
27+
changes.
28+
29+
## Development
30+
31+
This package is a [`uv` workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/) member
32+
of the main [`circulation`](../../README.md) repository. Work on it from the repo root — `uv sync`
33+
picks up all workspace members automatically; `tox -e py312-docker` runs the full test suite.
34+
Tests for this package live at the repository root under `tests/palace_util/` (workspace-member
35+
tests are kept under the root `tests/` tree so they share the repo's pytest fixtures and conftest
36+
plugins).
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[build-system]
2+
build-backend = "hatchling.build"
3+
requires = ["hatchling"]
4+
5+
[project]
6+
authors = [{name = "The Palace Project", email = "info@thepalaceproject.org"}]
7+
dependencies = [
8+
"python-dateutil>=2.8,<3",
9+
]
10+
description = "Shared utilities for Palace Project packages."
11+
license = "Apache-2.0"
12+
name = "palace-util"
13+
readme = "README.md"
14+
requires-python = ">=3.12,<4"
15+
version = "0"
16+
17+
[project.urls]
18+
Homepage = "https://thepalaceproject.org"
19+
Repository = "https://github.com/ThePalaceProject/circulation"
20+
21+
[tool.hatch.build.targets.wheel]
22+
packages = ["src/palace"]
23+
24+
[tool.isort]
25+
combine_as_imports = true
26+
known_first_party = ["palace.util"]
27+
profile = "black"
28+
sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER"

packages/palace-util/src/palace/util/__init__.py

Whitespace-only changes.
File renamed without changes.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from typing import Any
2+
3+
4+
class BasePalaceException(Exception):
5+
"""Base class for all Exceptions in Palace packages."""
6+
7+
def __init__(self, message: str | None = None):
8+
"""Initializes a new instance of BasePalaceException class
9+
10+
:param message: String containing description of the exception that occurred
11+
"""
12+
super().__init__(message)
13+
self.message = message
14+
15+
def __getstate__(self) -> dict[str, Any]:
16+
return {"dict": self.__dict__, "args": self.args}
17+
18+
def __setstate__(self, state: dict[str, Any] | None) -> None:
19+
# The signature must accept None to match BaseException.__setstate__ for mypy.
20+
# In practice, state is always a dict from our __getstate__ implementation.
21+
assert (
22+
state is not None
23+
), "__setstate__ received None; expected dict from __getstate__"
24+
self.__dict__.update(state["dict"])
25+
self.args = state["args"]
26+
27+
def __reduce__(self) -> tuple[Any, ...]:
28+
state = self.__getstate__()
29+
return self.__class__.__new__, (self.__class__,), state
30+
31+
32+
class PalaceValueError(BasePalaceException, ValueError): ...
33+
34+
35+
class PalaceTypeError(BasePalaceException, TypeError): ...

0 commit comments

Comments
 (0)