fastapi-unirate 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*.*.*"]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ outputs:
15
+ version: ${{ steps.version.outputs.version }}
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v3
21
+
22
+ - name: Set up Python
23
+ run: uv python install 3.12
24
+
25
+ - name: Verify tag matches package version
26
+ id: version
27
+ run: |
28
+ PKG_VERSION=$(grep -E '^version = ' pyproject.toml | head -1 | sed -E 's/version = "(.*)"/\1/')
29
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
30
+ echo "package version: $PKG_VERSION"
31
+ echo "tag version: $TAG_VERSION"
32
+ if [ "${{ github.event_name }}" = "push" ] && [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
33
+ echo "Tag $TAG_VERSION does not match pyproject.toml version $PKG_VERSION" >&2
34
+ exit 1
35
+ fi
36
+ echo "version=$PKG_VERSION" >> "$GITHUB_OUTPUT"
37
+
38
+ - name: Build sdist + wheel
39
+ run: uv build
40
+
41
+ - name: Upload build artifacts
42
+ uses: actions/upload-artifact@v4
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+
47
+ publish:
48
+ needs: build
49
+ runs-on: ubuntu-latest
50
+ environment:
51
+ name: pypi
52
+ url: https://pypi.org/p/fastapi-unirate
53
+ permissions:
54
+ id-token: write # Required for PyPI Trusted Publisher (OIDC)
55
+ steps:
56
+ - name: Download build artifacts
57
+ uses: actions/download-artifact@v4
58
+ with:
59
+ name: dist
60
+ path: dist/
61
+
62
+ - name: Publish to PyPI
63
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,40 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v3
24
+
25
+ - name: Set up Python ${{ matrix.python-version }}
26
+ run: uv python install ${{ matrix.python-version }}
27
+
28
+ - name: Install dependencies
29
+ run: uv sync --all-groups --python ${{ matrix.python-version }}
30
+
31
+ - name: Lint
32
+ run: |
33
+ uv run --group lint ruff check fastapi_unirate tests
34
+ uv run --group lint ruff format fastapi_unirate tests --diff
35
+
36
+ - name: Type check
37
+ run: uv run --group typing mypy fastapi_unirate tests
38
+
39
+ - name: Run unit tests
40
+ run: uv run --group test pytest tests/ -v
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+
6
+ .venv/
7
+ venv/
8
+ env/
9
+
10
+ build/
11
+ dist/
12
+ *.egg-info/
13
+ *.egg
14
+
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .coverage
19
+ htmlcov/
20
+
21
+ .DS_Store
22
+ .idea/
23
+ .vscode/
24
+
25
+ uv.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 UniRate API
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-unirate
3
+ Version: 0.1.0
4
+ Summary: FastAPI integration for the UniRate currency-exchange API — async dependency, lifespan manager, Money Pydantic type, and response-conversion middleware.
5
+ Project-URL: Homepage, https://github.com/UniRate-API/fastapi-unirate
6
+ Project-URL: Repository, https://github.com/UniRate-API/fastapi-unirate
7
+ Project-URL: Issues, https://github.com/UniRate-API/fastapi-unirate/issues
8
+ Project-URL: Provider, https://unirateapi.com
9
+ Author: Unirate Team
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: currency,exchange-rates,fastapi,fintech,forex,money,unirate
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Topic :: Office/Business :: Financial
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Requires-Python: >=3.9
28
+ Requires-Dist: fastapi<2.0,>=0.100
29
+ Requires-Dist: httpx<1.0,>=0.25
30
+ Requires-Dist: pydantic<3.0,>=2.0
31
+ Description-Content-Type: text/markdown
32
+
33
+ # fastapi-unirate
34
+
35
+ [![PyPI](https://img.shields.io/pypi/v/fastapi-unirate.svg)](https://pypi.org/project/fastapi-unirate/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/fastapi-unirate.svg)](https://pypi.org/project/fastapi-unirate/)
37
+ [![License](https://img.shields.io/pypi/l/fastapi-unirate.svg)](https://github.com/UniRate-API/fastapi-unirate/blob/main/LICENSE)
38
+
39
+ FastAPI integration for the [UniRate](https://unirateapi.com) currency-exchange API:
40
+
41
+ - **Async dependency** — `Depends(get_unirate_client)` plus a typed
42
+ `UniRateDep` alias for path-operation signatures.
43
+ - **Lifespan helper** — one shared `httpx.AsyncClient`-backed UniRate client
44
+ per app, opened on startup and closed on shutdown.
45
+ - **`Money` Pydantic type** — amount + currency pair that round-trips
46
+ through OpenAPI schemas.
47
+ - **`CurrencyConversionMiddleware`** — auto-rewrites Money-shaped values in
48
+ JSON responses to a request-scoped target currency
49
+ (`?currency=EUR`).
50
+
51
+ UniRate covers 593+ fiat, crypto, and commodity codes. Latest rates and
52
+ conversion are on the free tier; historical endpoints
53
+ (`convert_historical`) require Pro.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install fastapi-unirate
59
+ ```
60
+
61
+ Or with uv / Poetry:
62
+
63
+ ```bash
64
+ uv add fastapi-unirate
65
+ poetry add fastapi-unirate
66
+ ```
67
+
68
+ ## Quick start
69
+
70
+ ```python
71
+ import os
72
+
73
+ from fastapi import FastAPI
74
+ from fastapi_unirate import (
75
+ CurrencyConversionMiddleware,
76
+ Money,
77
+ UniRateDep,
78
+ unirate_lifespan,
79
+ )
80
+
81
+ app = FastAPI(lifespan=unirate_lifespan()) # reads UNIRATE_API_KEY from env
82
+ app.add_middleware(CurrencyConversionMiddleware)
83
+
84
+
85
+ @app.get("/products/widget")
86
+ async def get_widget() -> dict[str, Money]:
87
+ return {"price": Money(amount=19.99, currency="USD")}
88
+
89
+
90
+ @app.get("/rate/{base}/{quote}")
91
+ async def rate(base: str, quote: str, client: UniRateDep) -> dict[str, float]:
92
+ return {"rate": await client.get_rate(base, quote)}
93
+ ```
94
+
95
+ Then:
96
+
97
+ ```
98
+ $ curl localhost:8000/products/widget
99
+ {"price":{"amount":19.99,"currency":"USD"}}
100
+
101
+ $ curl localhost:8000/products/widget?currency=EUR
102
+ {"price":{"amount":18.42,"currency":"EUR"}}
103
+
104
+ $ curl localhost:8000/rate/USD/JPY
105
+ {"rate":151.83}
106
+ ```
107
+
108
+ The middleware finds every `{"amount": <number>, "currency": "<code>"}`
109
+ shape — top-level, nested, or inside lists — and rewrites it. Conversions
110
+ are de-duplicated per request and run concurrently.
111
+
112
+ ## Configuration
113
+
114
+ `unirate_lifespan` accepts overrides:
115
+
116
+ ```python
117
+ app = FastAPI(
118
+ lifespan=unirate_lifespan(
119
+ api_key="...", # default: $UNIRATE_API_KEY
120
+ base_url="https://api.unirateapi.com", # default: production
121
+ timeout=15.0, # default: 30s
122
+ )
123
+ )
124
+ ```
125
+
126
+ `CurrencyConversionMiddleware` accepts the query-parameter name and an
127
+ explicit client (mostly for testing):
128
+
129
+ ```python
130
+ app.add_middleware(
131
+ CurrencyConversionMiddleware,
132
+ query_param="display_currency",
133
+ )
134
+ ```
135
+
136
+ ## Why a middleware?
137
+
138
+ The dossier sketched two integration shapes — DI and middleware — and
139
+ both pull their weight:
140
+
141
+ - The DI provider is the right tool when a handler explicitly wants to
142
+ call `convert()` or `get_rate()`.
143
+ - The middleware is the right tool when a service has a stable
144
+ `Money`-shaped response and wants to expose
145
+ *one* `?currency=` knob to clients without rewriting every handler.
146
+
147
+ You can use either, or both together (as in the quick-start example).
148
+
149
+ ## Errors
150
+
151
+ The client raises `UniRateAPIError` on non-2xx responses (with
152
+ `status_code` set). Common cases:
153
+
154
+ | HTTP | Meaning |
155
+ |------|-------------------------------------------------|
156
+ | 401 | Missing or invalid API key |
157
+ | 403 | Pro subscription required (historical, commodities) |
158
+ | 404 | Currency not found |
159
+ | 429 | Rate limit exceeded |
160
+
161
+ The middleware swallows `UniRateAPIError` and passes the original
162
+ response through unchanged — so an upstream UniRate outage never becomes
163
+ a 500 on your service.
164
+
165
+ ## Compatibility
166
+
167
+ - Python 3.9 – 3.13
168
+ - FastAPI ≥ 0.100
169
+ - Pydantic ≥ 2.0
170
+
171
+ ## Related
172
+
173
+ - [`unirate-api`](https://pypi.org/project/unirate-api/) — sync UniRate
174
+ Python client (this package vendors a small async equivalent so it
175
+ doesn't pull in `requests`).
176
+ - [`langchain-unirate`](https://pypi.org/project/langchain-unirate/) —
177
+ LangChain partner package.
178
+ - Other UniRate integrations: dbt, Airflow, n8n, Raycast, MCP server.
179
+ Full list at <https://unirateapi.com>.
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,151 @@
1
+ # fastapi-unirate
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/fastapi-unirate.svg)](https://pypi.org/project/fastapi-unirate/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/fastapi-unirate.svg)](https://pypi.org/project/fastapi-unirate/)
5
+ [![License](https://img.shields.io/pypi/l/fastapi-unirate.svg)](https://github.com/UniRate-API/fastapi-unirate/blob/main/LICENSE)
6
+
7
+ FastAPI integration for the [UniRate](https://unirateapi.com) currency-exchange API:
8
+
9
+ - **Async dependency** — `Depends(get_unirate_client)` plus a typed
10
+ `UniRateDep` alias for path-operation signatures.
11
+ - **Lifespan helper** — one shared `httpx.AsyncClient`-backed UniRate client
12
+ per app, opened on startup and closed on shutdown.
13
+ - **`Money` Pydantic type** — amount + currency pair that round-trips
14
+ through OpenAPI schemas.
15
+ - **`CurrencyConversionMiddleware`** — auto-rewrites Money-shaped values in
16
+ JSON responses to a request-scoped target currency
17
+ (`?currency=EUR`).
18
+
19
+ UniRate covers 593+ fiat, crypto, and commodity codes. Latest rates and
20
+ conversion are on the free tier; historical endpoints
21
+ (`convert_historical`) require Pro.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install fastapi-unirate
27
+ ```
28
+
29
+ Or with uv / Poetry:
30
+
31
+ ```bash
32
+ uv add fastapi-unirate
33
+ poetry add fastapi-unirate
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ import os
40
+
41
+ from fastapi import FastAPI
42
+ from fastapi_unirate import (
43
+ CurrencyConversionMiddleware,
44
+ Money,
45
+ UniRateDep,
46
+ unirate_lifespan,
47
+ )
48
+
49
+ app = FastAPI(lifespan=unirate_lifespan()) # reads UNIRATE_API_KEY from env
50
+ app.add_middleware(CurrencyConversionMiddleware)
51
+
52
+
53
+ @app.get("/products/widget")
54
+ async def get_widget() -> dict[str, Money]:
55
+ return {"price": Money(amount=19.99, currency="USD")}
56
+
57
+
58
+ @app.get("/rate/{base}/{quote}")
59
+ async def rate(base: str, quote: str, client: UniRateDep) -> dict[str, float]:
60
+ return {"rate": await client.get_rate(base, quote)}
61
+ ```
62
+
63
+ Then:
64
+
65
+ ```
66
+ $ curl localhost:8000/products/widget
67
+ {"price":{"amount":19.99,"currency":"USD"}}
68
+
69
+ $ curl localhost:8000/products/widget?currency=EUR
70
+ {"price":{"amount":18.42,"currency":"EUR"}}
71
+
72
+ $ curl localhost:8000/rate/USD/JPY
73
+ {"rate":151.83}
74
+ ```
75
+
76
+ The middleware finds every `{"amount": <number>, "currency": "<code>"}`
77
+ shape — top-level, nested, or inside lists — and rewrites it. Conversions
78
+ are de-duplicated per request and run concurrently.
79
+
80
+ ## Configuration
81
+
82
+ `unirate_lifespan` accepts overrides:
83
+
84
+ ```python
85
+ app = FastAPI(
86
+ lifespan=unirate_lifespan(
87
+ api_key="...", # default: $UNIRATE_API_KEY
88
+ base_url="https://api.unirateapi.com", # default: production
89
+ timeout=15.0, # default: 30s
90
+ )
91
+ )
92
+ ```
93
+
94
+ `CurrencyConversionMiddleware` accepts the query-parameter name and an
95
+ explicit client (mostly for testing):
96
+
97
+ ```python
98
+ app.add_middleware(
99
+ CurrencyConversionMiddleware,
100
+ query_param="display_currency",
101
+ )
102
+ ```
103
+
104
+ ## Why a middleware?
105
+
106
+ The dossier sketched two integration shapes — DI and middleware — and
107
+ both pull their weight:
108
+
109
+ - The DI provider is the right tool when a handler explicitly wants to
110
+ call `convert()` or `get_rate()`.
111
+ - The middleware is the right tool when a service has a stable
112
+ `Money`-shaped response and wants to expose
113
+ *one* `?currency=` knob to clients without rewriting every handler.
114
+
115
+ You can use either, or both together (as in the quick-start example).
116
+
117
+ ## Errors
118
+
119
+ The client raises `UniRateAPIError` on non-2xx responses (with
120
+ `status_code` set). Common cases:
121
+
122
+ | HTTP | Meaning |
123
+ |------|-------------------------------------------------|
124
+ | 401 | Missing or invalid API key |
125
+ | 403 | Pro subscription required (historical, commodities) |
126
+ | 404 | Currency not found |
127
+ | 429 | Rate limit exceeded |
128
+
129
+ The middleware swallows `UniRateAPIError` and passes the original
130
+ response through unchanged — so an upstream UniRate outage never becomes
131
+ a 500 on your service.
132
+
133
+ ## Compatibility
134
+
135
+ - Python 3.9 – 3.13
136
+ - FastAPI ≥ 0.100
137
+ - Pydantic ≥ 2.0
138
+
139
+ ## Related
140
+
141
+ - [`unirate-api`](https://pypi.org/project/unirate-api/) — sync UniRate
142
+ Python client (this package vendors a small async equivalent so it
143
+ doesn't pull in `requests`).
144
+ - [`langchain-unirate`](https://pypi.org/project/langchain-unirate/) —
145
+ LangChain partner package.
146
+ - Other UniRate integrations: dbt, Airflow, n8n, Raycast, MCP server.
147
+ Full list at <https://unirateapi.com>.
148
+
149
+ ## License
150
+
151
+ MIT
@@ -0,0 +1,49 @@
1
+ """End-to-end FastAPI demo for fastapi-unirate.
2
+
3
+ Run:
4
+
5
+ UNIRATE_API_KEY=... uvicorn examples.example_app:app
6
+
7
+ Then:
8
+
9
+ curl http://127.0.0.1:8000/products/widget
10
+ curl 'http://127.0.0.1:8000/products/widget?currency=EUR'
11
+ curl http://127.0.0.1:8000/rate/USD/JPY
12
+ curl http://127.0.0.1:8000/convert/USD/EUR/100
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from fastapi import FastAPI
18
+
19
+ from fastapi_unirate import (
20
+ CurrencyConversionMiddleware,
21
+ Money,
22
+ UniRateDep,
23
+ unirate_lifespan,
24
+ )
25
+
26
+ app = FastAPI(
27
+ title="fastapi-unirate example",
28
+ lifespan=unirate_lifespan(),
29
+ )
30
+ app.add_middleware(CurrencyConversionMiddleware)
31
+
32
+
33
+ @app.get("/products/widget")
34
+ async def get_widget() -> dict[str, Money]:
35
+ """Demonstrates the conversion middleware: hit ?currency=EUR to rewrite."""
36
+ return {"price": Money(amount=19.99, currency="USD")}
37
+
38
+
39
+ @app.get("/rate/{base}/{quote}")
40
+ async def get_rate(base: str, quote: str, client: UniRateDep) -> dict[str, float]:
41
+ """Demonstrates the dependency: directly call the client."""
42
+ return {"rate": await client.get_rate(base, quote)}
43
+
44
+
45
+ @app.get("/convert/{base}/{quote}/{amount}")
46
+ async def convert(
47
+ base: str, quote: str, amount: float, client: UniRateDep
48
+ ) -> dict[str, float]:
49
+ return {"result": await client.convert(base, quote, amount)}
@@ -0,0 +1,22 @@
1
+ """FastAPI integration for the UniRate currency-exchange API."""
2
+
3
+ from fastapi_unirate.client import UniRateAPIError, UniRateClient
4
+ from fastapi_unirate.dependencies import (
5
+ UniRateDep,
6
+ get_unirate_client,
7
+ unirate_lifespan,
8
+ )
9
+ from fastapi_unirate.middleware import CurrencyConversionMiddleware
10
+ from fastapi_unirate.models import Money
11
+
12
+ __all__ = [
13
+ "CurrencyConversionMiddleware",
14
+ "Money",
15
+ "UniRateAPIError",
16
+ "UniRateClient",
17
+ "UniRateDep",
18
+ "get_unirate_client",
19
+ "unirate_lifespan",
20
+ ]
21
+
22
+ __version__ = "0.1.0"