Metadata-Version: 2.5
Name: acemq-amqp-fastapi
Version: 0.1.3
Summary: FastAPI integration for AceMQ AMQP: a lifespan that owns the connection and the consumers, and dependencies for publishing from a route
Project-URL: Homepage, https://acemq.org
Project-URL: Source, https://github.com/AceMQ-Company/acemq-python-amqp-fastapi
Project-URL: Issues, https://github.com/AceMQ-Company/acemq-python-amqp-fastapi/issues
Author: AceMQ
License:                                  Apache License
                                   Version 2.0, January 2004
                                http://www.apache.org/licenses/
        
           TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
        
           1. Definitions.
        
              "License" shall mean the terms and conditions for use, reproduction,
              and distribution as defined by Sections 1 through 9 of this document.
        
              "Licensor" shall mean the copyright owner or entity authorized by
              the copyright owner that is granting the License.
        
              "Legal Entity" shall mean the union of the acting entity and all
              other entities that control, are controlled by, or are under common
              control with that entity. For the purposes of this definition,
              "control" means (i) the power, direct or indirect, to cause the
              direction or management of such entity, whether by contract or
              otherwise, or (ii) ownership of fifty percent (50%) or more of the
              outstanding shares, or (iii) beneficial ownership of such entity.
        
              "You" (or "Your") shall mean an individual or Legal Entity
              exercising permissions granted by this License.
        
              "Source" form shall mean the preferred form for making modifications,
              including but not limited to software source code, documentation
              source, and configuration files.
        
              "Object" form shall mean any form resulting from mechanical
              transformation or translation of a Source form, including but
              not limited to compiled object code, generated documentation,
              and conversions to other media types.
        
              "Work" shall mean the work of authorship, whether in Source or
              Object form, made available under the License, as indicated by a
              copyright notice that is included in or attached to the work
              (an example is provided in the Appendix below).
        
              "Derivative Works" shall mean any work, whether in Source or Object
              form, that is based on (or derived from) the Work and for which the
              editorial revisions, annotations, elaborations, or other modifications
              represent, as a whole, an original work of authorship. For the purposes
              of this License, Derivative Works shall not include works that remain
              separable from, or merely link (or bind by name) to the interfaces of,
              the Work and Derivative Works thereof.
        
              "Contribution" shall mean any work of authorship, including
              the original version of the Work and any modifications or additions
              to that Work or Derivative Works thereof, that is intentionally
              submitted to Licensor for inclusion in the Work by the copyright owner
              or by an individual or Legal Entity authorized to submit on behalf of
              the copyright owner. For the purposes of this definition, "submitted"
              means any form of electronic, verbal, or written communication sent
              to the Licensor or its representatives, including but not limited to
              communication on electronic mailing lists, source code control systems,
              and issue tracking systems that are managed by, or on behalf of, the
              Licensor for the purpose of discussing and improving the Work, but
              excluding communication that is conspicuously marked or otherwise
              designated in writing by the copyright owner as "Not a Contribution."
        
              "Contributor" shall mean Licensor and any individual or Legal Entity
              on behalf of whom a Contribution has been received by Licensor and
              subsequently incorporated within the Work.
        
           2. Grant of Copyright License. Subject to the terms and conditions of
              this License, each Contributor hereby grants to You a perpetual,
              worldwide, non-exclusive, no-charge, royalty-free, irrevocable
              copyright license to reproduce, prepare Derivative Works of,
              publicly display, publicly perform, sublicense, and distribute the
              Work and such Derivative Works in Source or Object form.
        
           3. Grant of Patent License. Subject to the terms and conditions of
              this License, each Contributor hereby grants to You a perpetual,
              worldwide, non-exclusive, no-charge, royalty-free, irrevocable
              (except as stated in this section) patent license to make, have made,
              use, offer to sell, sell, import, and otherwise transfer the Work,
              where such license applies only to those patent claims licensable
              by such Contributor that are necessarily infringed by their
              Contribution(s) alone or by combination of their Contribution(s)
              with the Work to which such Contribution(s) was submitted. If You
              institute patent litigation against any entity (including a
              cross-claim or counterclaim in a lawsuit) alleging that the Work
              or a Contribution incorporated within the Work constitutes direct
              or contributory patent infringement, then any patent licenses
              granted to You under this License for that Work shall terminate
              as of the date such litigation is filed.
        
           4. Redistribution. You may reproduce and distribute copies of the
              Work or Derivative Works thereof in any medium, with or without
              modifications, and in Source or Object form, provided that You
              meet the following conditions:
        
              (a) You must give any other recipients of the Work or
                  Derivative Works a copy of this License; and
        
              (b) You must cause any modified files to carry prominent notices
                  stating that You changed the files; and
        
              (c) You must retain, in the Source form of any Derivative Works
                  that You distribute, all copyright, patent, trademark, and
                  attribution notices from the Source form of the Work,
                  excluding those notices that do not pertain to any part of
                  the Derivative Works; and
        
              (d) If the Work includes a "NOTICE" text file as part of its
                  distribution, then any Derivative Works that You distribute must
                  include a readable copy of the attribution notices contained
                  within such NOTICE file, excluding those notices that do not
                  pertain to any part of the Derivative Works, in at least one
                  of the following places: within a NOTICE text file distributed
                  as part of the Derivative Works; within the Source form or
                  documentation, if provided along with the Derivative Works; or,
                  within a display generated by the Derivative Works, if and
                  wherever such third-party notices normally appear. The contents
                  of the NOTICE file are for informational purposes only and
                  do not modify the License. You may add Your own attribution
                  notices within Derivative Works that You distribute, alongside
                  or as an addendum to the NOTICE text from the Work, provided
                  that such additional attribution notices cannot be construed
                  as modifying the License.
        
              You may add Your own copyright statement to Your modifications and
              may provide additional or different license terms and conditions
              for use, reproduction, or distribution of Your modifications, or
              for any such Derivative Works as a whole, provided Your use,
              reproduction, and distribution of the Work otherwise complies with
              the conditions stated in this License.
        
           5. Submission of Contributions. Unless You explicitly state otherwise,
              any Contribution intentionally submitted for inclusion in the Work
              by You to the Licensor shall be under the terms and conditions of
              this License, without any additional terms or conditions.
              Notwithstanding the above, nothing herein shall supersede or modify
              the terms of any separate license agreement you may have executed
              with Licensor regarding such Contributions.
        
           6. Trademarks. This License does not grant permission to use the trade
              names, trademarks, service marks, or product names of the Licensor,
              except as required for reasonable and customary use in describing the
              origin of the Work and reproducing the content of the NOTICE file.
        
           7. Disclaimer of Warranty. Unless required by applicable law or
              agreed to in writing, Licensor provides the Work (and each
              Contributor provides its Contributions) on an "AS IS" BASIS,
              WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
              implied, including, without limitation, any warranties or conditions
              of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
              PARTICULAR PURPOSE. You are solely responsible for determining the
              appropriateness of using or redistributing the Work and assume any
              risks associated with Your exercise of permissions under this License.
        
           8. Limitation of Liability. In no event and under no legal theory,
              whether in tort (including negligence), contract, or otherwise,
              unless required by applicable law (such as deliberate and grossly
              negligent acts) or agreed to in writing, shall any Contributor be
              liable to You for damages, including any direct, indirect, special,
              incidental, or consequential damages of any character arising as a
              result of this License or out of the use or inability to use the
              Work (including but not limited to damages for loss of goodwill,
              work stoppage, computer failure or malfunction, or any and all
              other commercial damages or losses), even if such Contributor
              has been advised of the possibility of such damages.
        
           9. Accepting Warranty or Additional Liability. While redistributing
              the Work or Derivative Works thereof, You may choose to offer,
              and charge a fee for, acceptance of support, warranty, indemnity,
              or other liability obligations and/or rights consistent with this
              License. However, in accepting such obligations, You may act only
              on Your own behalf and on Your sole responsibility, not on behalf
              of any other Contributor, and only if You agree to indemnify,
              defend, and hold each Contributor harmless for any liability
              incurred by, or claims asserted against, such Contributor by reason
              of your accepting any such warranty or additional liability.
        
           END OF TERMS AND CONDITIONS
        
           APPENDIX: How to apply the Apache License to your work.
        
              To apply the Apache License to your work, attach the following
              boilerplate notice, with the fields enclosed by brackets "[]"
              replaced with your own identifying information. (Don't include
              the brackets!)  The text should be enclosed in the appropriate
              comment syntax for the file format. We also recommend that a
              file or class name and description of purpose be included on the
              same "printed page" as the copyright notice for easier
              identification within third-party archives.
        
           Copyright [yyyy] [name of copyright owner]
        
           Licensed under the Apache License, Version 2.0 (the "License");
           you may not use this file except in compliance with the License.
           You may obtain a copy of the License at
        
               http://www.apache.org/licenses/LICENSE-2.0
        
           Unless required by applicable law or agreed to in writing, software
           distributed under the License is distributed on an "AS IS" BASIS,
           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
           See the License for the specific language governing permissions and
           limitations under the License.
License-File: LICENSE
License-File: NOTICE
Keywords: acemq,amqp,asgi,fastapi,messaging,rabbitmq
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Networking
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: acemq-amqp[rabbitmq]<0.8,>=0.7.10
Requires-Dist: fastapi>=0.110
Requires-Dist: pydantic-settings>=2.2
Provides-Extra: dev
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: uvicorn>=0.29; extra == 'dev'
Description-Content-Type: text/markdown

# acemq-python-amqp-fastapi

The FastAPI integration for AceMQ: a lifespan that owns the connection and the
consumers, `Depends` for publishing from a route, a decorator for registering handlers,
and a health check that knows a blocked broker is not a reason to restart.

It is the Python analogue of the
[Spring Boot starter](https://github.com/AceMQ-Company/acemq-java-amqp-spring-boot-starter),
and it occupies the same seam. What Spring gives a starter is `SmartLifecycle` —
something that runs after the context is built and before it is torn down. What FastAPI
gives is `lifespan`. They are the same idea, and this package lives in it.

```python
from typing import Annotated

from acemq_amqp import Ack, Message, Publisher, accept
from acemq_fastapi import AceMQ, publishes
from fastapi import Depends, FastAPI

acemq = AceMQ()


@acemq.consumer("orders")
async def handle(message: Message) -> Ack:
    await warehouse.reserve(message.payload)
    return accept()


app = FastAPI(lifespan=acemq.lifespan)
app.include_router(acemq.health_router())

Orders = Annotated[Publisher, Depends(publishes("orders"))]


@app.post("/orders")
async def place(orders: Orders) -> dict[str, str]:
    result = await orders.send({"sku": "A-1"})
    return {"id": result.message_id}
```

## Installing

```bash
pip install --extra-index-url https://acemq.org/pypi/simple/ acemq-amqp-fastapi
```

The extra index is needed because AceMQ is not on PyPI before 1.0. It brings
`acemq-amqp[rabbitmq]`, FastAPI and `pydantic-settings` with it — no second install.

Python 3.10 or newer, which is the library's floor. The dependency on the library is
`>=0.7.10,<0.8` — a range rather than a pin, so pip resolves the newest release inside it
and the range is also the constraint your application inherits. The current library
release is **0.7.10**, and that is what this repository's CI tests against: its whole job
is proving the *published* package works, so it installs it from the index like anybody
else.

In a `requirements.txt` the index goes in the file:

```
--extra-index-url https://acemq.org/pypi/simple/
acemq-amqp-fastapi==0.1.3
```

## What it wires up

| | When | |
|---|---|---|
| The connection | The lifespan opens | Opened under `acemq.connection_timeout`, drained and closed on the way out |
| The codec | Always | `acemq.format`, json by default |
| The topology | `acemq.topology` declares something | Nothing declared means nothing applied, which is a real answer |
| Consumers | Every `@acemq.consumer` | Started on the application's own event loop, after the topology |
| Publishers | Per destination, on first use | Cached, and cleared with the connection |
| `app.state.acemq` | The lifespan opens | How the dependencies find it without a global |
| A health check | `acemq.health_check()` / `acemq.health_router()` | Composes into an aggregate, or answers on its own path |

Every one of them takes the connection rather than creating one, so
`AceMQ(connection_factory=...)` replaces the connection and everything else keeps
working — the same shape as the Spring starter's `@ConditionalOnMissingBean`.

## Three decisions worth knowing about

### The lifespan composes rather than replaces

FastAPI takes one lifespan, and an application that already has one should not have to
give it up:

```python
app = FastAPI(lifespan=compose_lifespans(database_pool, acemq.lifespan))
```

Left to right is outside in. **Put AceMQ last** — consumers should start after the
things their handlers use and stop before those go away.

### A blocked connection reports healthy, with the reason

A blocked connection is the broker protecting itself, usually from disk or memory
pressure. An application that fails its own readiness check for it is one an
orchestrator restarts into the same blocked broker, having thrown away whatever it was
holding. So the check reports **up**, with `parts.blocked` true and the reason in
`detail`, and 503 is reserved for a broker that did not answer at all.

That detail is `the broker has blocked this connection; publishing is paused` — the
same sentence every AceMQ library writes, so one alert rule matches a blocked broker
whatever language the service is in. It comes from the connection and is passed through
rather than paraphrased. On RabbitMQ nothing follows it: the broker does send a reason,
aiormq logs it and keeps only a flag, so `blocked_reason` is `null` and inventing one
would read exactly like a reason the broker sent.

`acemq.health.timeout` is handed to the library's probe as its deadline rather than
wrapped around it, so a blocked broker that stops answering is still described as
blocked rather than as silence. A blocked broker is not probed at all.

### `publisher_confirms=false` is refused, not ignored

The Python library publishes with confirms always. A setting that silently did nothing
would leave a service believing it had traded safety for throughput and having neither,
so the setting exists only to raise with an explanation.

## What a drain finishes

Measured, on this version, against RabbitMQ 4. The full numbers are in
[docs/shutdown.md](docs/shutdown.md).

| at the moment the lifespan exits | what happens |
|---|---|
| a handler is running | it runs to completion, and its decision is carried out |
| a delivery has arrived but no handler has it yet | **it goes back to the broker**, requeued — unlike Go, which handles them all |
| a publish is waiting for its confirm | it is cut off, and raises `PublishError` naming how many were unconfirmed |
| a retry is waiting out a backoff in this process | the drain waits with it, for the whole delay |

Two of those deserve a sentence here. The second means a large prefetch costs
**redeliveries** at shutdown rather than a long drain, which is the opposite of the Go
library's trade — keep handlers idempotent and it stops mattering. The fourth means a
retry ladder without `wait_in_broker_from` set can make a drain take as long as its
longest delay, which is how a deployment starts timing out for reasons nobody connects
to the retry policy.

Bounded by `acemq.consumer.shutdown_timeout`, twenty seconds by default. When it
expires the handlers still running are cancelled and their messages left unsettled — the
broker redelivers them, so work may be done twice and nothing is dropped.

## What it is not

**This is FastAPI, not Django, and that is not an oversight.** The whole design rests
on one event loop the framework owns and hands to the lifespan: consumers are `asyncio`
tasks created on it, handlers await on it, and the drain runs on it while the server is
closing its sockets. Django's synchronous path has no such seam — `AppConfig.ready()`
runs per worker process with no loop and no shutdown hook, and a management command
that starts consumers is a second process with a different lifetime and a different
deployment. A Django integration is a different design with a different set of honest
limits. Pretending one package could be both would produce something good at neither.

**It is not a second messaging library.** Messages, acknowledgements, codecs, retry
policies, topologies, interceptors and patterns are all `acemq-amqp`'s, under their own
names. This package re-exports none of them. What `accept()` does, or where a message
that failed five times ends up, is answered by
[the library's documentation](https://acemq.org/acemq-python-amqp/).

**It does not own your `/health`.** It contributes one check. Whether that becomes the
whole readiness probe or one part of an aggregate beside your database is yours.

**It does not make a publish survive a restart.** A publish that must not be lost when
the process dies wants the library's outbox, and no lifespan is a substitute for one.

**Consumers share the process with your routes.** A handler that blocks the event loop
stalls the HTTP server as surely as it stalls the other handlers. That is the cost of
running an API and a consumer in one process, and this package does not hide it — see
[docs/consumers.md](docs/consumers.md) for the thread pool, and `acemq.start()` with no
application for the case where the handlers deserve their own process.

## Documentation

Seventeen pages, published at
**<https://acemq.org/acemq-python-amqp-fastapi/>**. They read as markdown in
[docs/](docs/) too, and render with `.github/scripts/build-docs-site.sh`.

| | |
|---|---|
| **Start here** | [docs/index.md](docs/index.md) · [Getting started](docs/getting-started.md) |
| **Reference** | [Configuration](docs/configuration.md) — every `acemq.*` setting |
| **Usage** | [The lifespan](docs/lifespan.md) · [Consumers](docs/consumers.md) · [Publishing from a route](docs/publishing.md) · [Testing](docs/testing.md) |
| **Patterns** | [Patterns from FastAPI](docs/patterns.md) · [Request-reply](docs/request-reply.md) · [Streams](docs/streams.md) · [Serialization](docs/serialization.md) |
| **Operations** | [Security](docs/security.md) · [Topology](docs/topology.md) · [Health](docs/health.md) · [Observability](docs/observability.md) · [Shutdown](docs/shutdown.md) |
| **Support** | [Enterprise support](https://acemq.com) |

[Patterns from FastAPI](docs/patterns.md) is the one to read after Getting started. It
lists every pattern `acemq-amqp` carries — idempotent consumer, outbox, saga, claim check,
scheduler, replay, routing slip — and says for each whether there is an `acemq.*` setting
or whether it is four lines in a lifespan. For most of them it is the latter, and saying so
is more useful than a setting that pretended a database handle could come from an
environment variable.

## Its own version line

This repository versions separately from `acemq-amqp`, starting at 0.1.0, as the Spring
Boot starter does from `acemq-java-amqp`. It tracks FastAPI and Starlette's release
train as much as AceMQ's: a FastAPI release that moves the lifespan contract is a
release this package has to answer, and it should not have to wait for the library to
cut a version to do it.

## Developing

```bash
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]" --extra-index-url https://acemq.org/pypi/simple/

pytest -m "not integration"   # everything that needs no broker
ruff check . && mypy          # what CI runs

docker run -d --rm --name acemq-test -p 5722:5672 rabbitmq:4-alpine
pytest -m integration
```

### Releasing

```bash
git tag -a v0.1.0 -m "AceMQ for FastAPI 0.1.0" && git push origin v0.1.0
```

`release.yml` runs the whole suite including the integration tests, builds the sdist and
the wheel, reads the version and the `acemq-amqp` pin back out of the built wheel,
checks the sdist alone installs and imports, adds both files to the AceMQ package index,
then installs the published version from that index into an empty interpreter to confirm
consumers get what was built.

It publishes to **<https://acemq.org/pypi/>**, a static PEP 503 index served from
`AceMQ-Company/pypi` — not to PyPI. There is no upload token and no Trusted Publishing:
the job clones that repository with an SSH deploy key held in `PYPI_REPO_DEPLOY_KEY`,
adds the distributions, and pushes. A file on that index can be deleted and the index
regenerated, which is the property worth having at 0.1.x; a version on PyPI is
permanent.

Three guards. The version must be a version — an allow-list, because it reaches a file
name and a package version. It must be `0.1.x`, so a mistyped tag cannot claim a version
this package has not earned. And the tag is what names the release, so nothing publishes
unless `pyproject.toml` and `acemq_fastapi.__version__` both already say the same thing,
which means the version in the tree is bumped before the tag is cut rather than after.

Before the first tag this repository needs `PYPI_REPO_DEPLOY_KEY` — the private half of
a deploy key with write access to `AceMQ-Company/pypi` — and a `pypi-feed` environment.
Until then a tag reaches the publish job and fails there, naming the missing secret,
which is the right way round: nothing is published by accident and the failure says what
is missing. [RELEASING.md](RELEASING.md) has the whole procedure.

## Licence

Apache-2.0. See [LICENSE](LICENSE), and [docs/licence.md](docs/licence.md) for what the
warranty disclaimer means in practice.
