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-importsenforces 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.