Skip to main content

snakeboot

Spring Boot ergonomics for hexagonal Python applications built with Django, Django Ninja and wireup.

snakeboot imitates the basics of Spring Boot, not Spring itself. Each part is a handful of small classes you can read in an afternoon, and none of them knows anything about your domain.

snakeboot Spring Boot Needs
@transactional, after_commit @Transactional, @TransactionalEventListener(AFTER_COMMIT) nothing
@application_root, ComponentScan @SpringBootApplication, @ComponentScan nothing
ApplicationEventPublisher, @event_listener, @transactional_event_listener the same names wireup
@rest_controller, @route.get, @rest_controller_advice, @exception_handler @RestController, @GetMapping, @RestControllerAdvice, @ExceptionHandler snakeboot[ninja]
DjangoAtomicTransactionManager PlatformTransactionManager for JDBC snakeboot[django]
@configuration_properties, Section, load_configuration, application-<profile>.toml @ConfigurationProperties, @EnableConfigurationProperties, profiles snakeboot[configuration]
DjangoProperties, DatabaseProperties, CacheProperties server.*, spring.datasource.*, spring.data.redis.* snakeboot[configuration]
RecordingTransactionManager, declared_classes spring-test nothing

The transaction, component scan and testing packages import nothing but the standard library, so the application layer of a hexagonal application can use @transactional without importing Django.

Install

uv add "snakeboot[configuration,django,ninja]"

A tour

The application root. The component scan starts in the package of the marked class, like @SpringBootApplication. It skips Django's entry points (wsgi.py, asgi.py, migrations) and adds the components snakeboot provides itself (the event publisher).

# myapp/myapp_application.py
@application_root
class MyApplication: ...


# settings.py: the setting wireup needs anyway
WIREUP = WireupSettings(injectables=[*ComponentScan.of(MyApplication).module_names()], auto_inject_views=False)

Typed configuration. A Section (a frozen Pydantic model that refuses unknown keys) is bound to one table of application.toml, like @ConfigurationProperties. Settings lists the sections once, like @EnableConfigurationProperties; every section then reaches wireup by its type. The sections Django itself needs come with snakeboot: DjangoProperties ([django]), DatabaseProperties ([database], PostgreSQL) and CacheProperties ([cache], Redis).

@configuration_properties("chatbot")
class ChatbotProperties(Section):
    model: str  # no default: the value is in application.toml
    llm: LanguageModelProperties  # a nested Section is the table [chatbot.llm]


# settings.py
CONFIGURATION = load_configuration(DjangoProperties, DatabaseProperties, ChatbotProperties, package="myapp")
SECRET_KEY = CONFIGURATION.get(DjangoProperties).secret_key.get_secret_value()
DATABASES = {"default": CONFIGURATION.get(DatabaseProperties).as_django_database()}
WIREUP = WireupSettings(injectables=[*ComponentScan.of(MyApplication).module_names(), *CONFIGURATION.injectables()])

The values come from three places, a later one overriding an earlier one key by key:

Source Holds Spring Boot
application.toml in the package every value the application runs with in production application.yml
application-<profile>.toml, when SNAKEBOOT_PROFILE names one what differs on a developer machine, say application-local.yml, SPRING_PROFILES_ACTIVE
environment variables SECTION__KEY__SUBKEY secrets and addresses of one deployment relaxed binding

Nothing else is read: no .env file, no default in code, no command line. The start stops when a model has a default, ignores unknown keys or is mutable; when a file or a value is missing; when a key or a table is unknown (a typo in application.toml or in CHATBOT__LLM__MODLE). Tables are merged, not replaced, so put the variant without keys of its own in application.toml when a profile or the environment may choose another.

Transactions. A use case declares its boundary; composition chooses the technology.

@injectable
class CancelRegistrationUseCase:
    @transactional
    def execute(self, request: CancelRegistrationRequestDTO) -> None:
        workshop = self._workshops.get(...)
        workshop.cancel_registration(...)
        self._workshops.save(workshop)
        self._events.publish_all(workshop.pull_events())


# composition, at start-up
TransactionManagerRegistry.install(DjangoAtomicTransactionManager())

Application events. Listeners react to facts after the commit, so a rolled-back change never reaches anyone, and a failing listener cannot undo a committed one.

@application_event_listener
class ParticipantNotificationListener:
    def __init__(self, notifications: NotificationPort) -> None:
        self._notifications = notifications

    @transactional_event_listener(WaitlistedParticipantPromoted)
    def notify_promoted(self, event: WaitlistedParticipantPromoted) -> None: ...

REST controllers. Singleton classes with constructor injection, found by the component scan. Two endpoints for the same URL, or two answers for one error, stop the start-up.

@rest_controller("/workshops", tags=["workshops"])
class WorkshopRestController:
    def __init__(self, enrollment: EnrollmentFacadePort) -> None:
        self._enrollment = enrollment

    @route.get("/{workshop_id}", response=WorkshopViewDTO)
    def get_workshop(self, request: HttpRequest, workshop_id: str) -> WorkshopViewDTO: ...


@rest_controller_advice
class EnrollmentRestErrorAdvice:
    @exception_handler(WorkshopNotFoundError, status=404)
    def answer_not_found(self, error: Exception) -> dict[str, str]:
        return error_answer(error)  # {"code": "workshop_not_found", "detail": "..."}


# urls.py
mounted = RestControllerMounter().mount(api, scan=settings.WIREUP.injectables or ())

Design rules

  • Imitate the basics, not all of Spring: no AOP, no scheduling, no message brokers.
  • Fail at start-up, not at the first request: a broken controller, listener, root or configuration stops the application.
  • Each package knows as little as possible; uv run lint-imports enforces it.

Develop

uv sync
uv run pre-commit install
uv run pytest            # tests, including the import contracts
uv run ruff check        # Clean Code rules
uv run mypy              # strict

Metadata

Release files for snakeboot 0.3.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 snakeboot 0.3.0
File Size Uploaded Provenance Actions
snakeboot-0.3.0.tar.gz 28.5 kB Without provenance Details for snakeboot-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for snakeboot 0.3.0
File Interpreter ABI Platform Actions
snakeboot-0.3.0-py3-none-any.whl Python 3 none any Details for snakeboot-0.3.0-py3-none-any.whl

Total release size: 70.3 kB

Release files / snakeboot-0.3.0.tar.gz

Download URL snakeboot-0.3.0.tar.gz
Size 28.5 kB
Tags Source
SHA-256 checksum
How to use checksums
51137471ed9b5602edae0934e67fe0ac6146a46ae6163da9b5d342863f61ba8f
BLAKE2b-256 checksum
How to use checksums
32e92089c62d4dc18a4b2046580a71f9f1363d567704c0a4de46aaf52a8494ec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / snakeboot-0.3.0-py3-none-any.whl

Download URL snakeboot-0.3.0-py3-none-any.whl
Size 41.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
06f5c6c934642b593e6c1de5bc5df5208eeb73a85d60cb054e62c40e35e417a6
BLAKE2b-256 checksum
How to use checksums
5a9696e30b872957f9ce16306ed83b4a211015df713bb786e9bd63631ab0e20b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for snakeboot 0.3.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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

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