Skip to content

Commit 8ed457f

Browse files
rob-brownccclaude
andcommitted
feat: initial release of streamlit-unirate v0.1.0
Streamlit integration for the UniRate API with st.cache_resource / st.cache_data support built in: get_client(), get_rate(), convert(), list_currencies(), get_vat_rates(). Secrets resolved from st.secrets → env var. 22 pytest mock tests; ruff + mypy clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0 parents  commit 8ed457f

17 files changed

Lines changed: 2667 additions & 0 deletions

.github/workflows/release.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/streamlit-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

.github/workflows/test.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.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 streamlit_unirate tests
34+
uv run --group lint ruff format streamlit_unirate tests --diff
35+
36+
- name: Type check
37+
run: uv run --group typing mypy streamlit_unirate tests
38+
39+
- name: Run unit tests
40+
run: uv run --group test pytest tests/ -v

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Unirate Team
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.

README.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# streamlit-unirate
2+
3+
Streamlit integration for the [UniRate API](https://unirateapi.com) — live exchange rates, currency conversion, and VAT rates with automatic `st.cache_data` / `st.cache_resource` caching built in.
4+
5+
[![PyPI](https://img.shields.io/pypi/v/streamlit-unirate)](https://pypi.org/project/streamlit-unirate/)
6+
[![Python](https://img.shields.io/pypi/pyversions/streamlit-unirate)](https://pypi.org/project/streamlit-unirate/)
7+
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8+
[![CI](https://github.com/UniRate-API/streamlit-unirate/actions/workflows/test.yml/badge.svg)](https://github.com/UniRate-API/streamlit-unirate/actions)
9+
10+
## Installation
11+
12+
```bash
13+
pip install streamlit-unirate
14+
```
15+
16+
## Quick start
17+
18+
```python
19+
import streamlit as st
20+
import streamlit_unirate as ur
21+
22+
# List currencies (cached 1 h)
23+
currencies = ur.list_currencies()
24+
25+
from_cur = st.selectbox("From", currencies)
26+
to_cur = st.selectbox("To", currencies)
27+
amount = st.number_input("Amount", value=100.0)
28+
29+
if st.button("Convert"):
30+
result = ur.convert(to_cur, amount, from_cur)
31+
st.metric(f"{amount} {from_cur}", f"{result:.4f} {to_cur}")
32+
```
33+
34+
## API key
35+
36+
Set `UNIRATE_API_KEY` in `.streamlit/secrets.toml` (preferred) or as an environment variable:
37+
38+
```toml
39+
# .streamlit/secrets.toml
40+
UNIRATE_API_KEY = "your_api_key_here"
41+
```
42+
43+
```bash
44+
UNIRATE_API_KEY=your_key streamlit run app.py
45+
```
46+
47+
Get a free key at [unirateapi.com](https://unirateapi.com).
48+
49+
## API reference
50+
51+
All functions use Streamlit's caching automatically — no extra decorators needed.
52+
53+
### `get_client(api_key=None, base_url=None)`
54+
55+
Returns the underlying `unirate.UnirateClient` singleton (cached via `@st.cache_resource`). Use this when you need direct access to Pro endpoints such as historical rates or time series.
56+
57+
```python
58+
client = ur.get_client()
59+
data = client.get_time_series("2025-01-01", "2025-01-31", base_currency="USD")
60+
```
61+
62+
### `get_rate(from_currency="USD", to_currency=None)``float | dict`
63+
64+
Current exchange rate. Cached for **5 minutes**.
65+
66+
```python
67+
rate = ur.get_rate("USD", "EUR") # → 0.9214 (float)
68+
all_rates = ur.get_rate("USD") # → {"EUR": 0.92, "GBP": 0.79, ...}
69+
```
70+
71+
### `convert(to_currency, amount=1.0, from_currency="USD")``float`
72+
73+
Convert an amount. Cached for **5 minutes**.
74+
75+
```python
76+
eur = ur.convert("EUR", 250.0, "USD") # → 230.35
77+
```
78+
79+
### `list_currencies()``list[str]`
80+
81+
All supported currency codes. Cached for **1 hour**.
82+
83+
```python
84+
codes = ur.list_currencies() # → ["AED", "AFN", ..., "ZWL"]
85+
```
86+
87+
### `get_vat_rates(country=None)``dict`
88+
89+
VAT rates for all countries or a single ISO-3166 country code. Cached for **24 hours**.
90+
91+
```python
92+
all_vat = ur.get_vat_rates()
93+
de_vat = ur.get_vat_rates("DE")
94+
```
95+
96+
## Error handling
97+
98+
All functions raise exceptions from `unirate.exceptions`:
99+
100+
| Exception | Cause |
101+
|---|---|
102+
| `AuthenticationError` | Missing or invalid API key |
103+
| `RateLimitError` | Free-tier rate limit exceeded |
104+
| `InvalidCurrencyError` | Unknown currency code |
105+
| `APIError` | Other API error (check `.status_code`) |
106+
107+
```python
108+
from unirate.exceptions import RateLimitError, AuthenticationError
109+
import streamlit_unirate as ur
110+
111+
try:
112+
rate = ur.get_rate("USD", "EUR")
113+
except AuthenticationError:
114+
st.error("Check your UNIRATE_API_KEY.")
115+
except RateLimitError:
116+
st.warning("Rate limit hit — try again in a moment.")
117+
```
118+
119+
## Related UniRate clients
120+
121+
<!-- unirate-ecosystem-footer:start -->
122+
**Other UniRate integrations:**
123+
[Python](https://github.com/UniRate-API/unirate-api-python) · [Node.js](https://github.com/UniRate-API/unirate-api-nodejs) · [Go](https://github.com/UniRate-API/unirate-api-go) · [Rust](https://github.com/UniRate-API/unirate-api-rust) · [Ruby](https://github.com/UniRate-API/unirate-api-ruby) · [PHP](https://github.com/UniRate-API/unirate-api-php) · [Java](https://github.com/UniRate-API/unirate-api-java) · [Swift](https://github.com/UniRate-API/unirate-api-swift) · [.NET](https://github.com/UniRate-API/unirate-api-dotnet) · [FastAPI](https://github.com/UniRate-API/fastapi-unirate) · [Flask](https://github.com/UniRate-API/flask-unirate) · [Django REST](https://github.com/UniRate-API/djangorestframework-unirate) · [Wagtail](https://github.com/UniRate-API/wagtail-unirate) · [Next.js](https://github.com/UniRate-API/next-unirate) · [Nuxt](https://github.com/UniRate-API/nuxt-unirate) · [SvelteKit](https://github.com/UniRate-API/sveltekit-unirate) · [React](https://github.com/UniRate-API/react-unirate) · [Vue](https://github.com/UniRate-API/vue-unirate) · [Angular](https://github.com/UniRate-API/angular-unirate) · [Remix](https://github.com/UniRate-API/remix-unirate) · [NestJS](https://github.com/UniRate-API/nestjs-unirate) · [LangChain (Python)](https://github.com/UniRate-API/langchain-unirate) · [LangChain.js](https://github.com/UniRate-API/langchain-js-unirate) · [Astro](https://github.com/UniRate-API/astro-unirate) · [Eleventy](https://github.com/UniRate-API/eleventy-unirate) · [Hugo](https://github.com/UniRate-API/hugo-unirate) · [Jekyll](https://github.com/UniRate-API/jekyll-unirate) · [Strapi](https://github.com/UniRate-API/strapi-plugin-unirate) · [Directus](https://github.com/UniRate-API/directus-extension-unirate) · [Medusa](https://github.com/UniRate-API/medusa-plugin-unirate) · [MCP Server](https://github.com/UniRate-API/unirate-mcp) · [CLI](https://github.com/UniRate-API/unirate-cli)
124+
<!-- unirate-ecosystem-footer:end -->
125+
126+
## License
127+
128+
MIT — see [LICENSE](LICENSE).

examples/basic_app.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Example Streamlit app using streamlit-unirate.
2+
3+
Run with:
4+
UNIRATE_API_KEY=your_key streamlit run examples/basic_app.py
5+
"""
6+
7+
import streamlit as st
8+
import streamlit_unirate as ur
9+
10+
st.title("Currency Converter")
11+
12+
currencies = ur.list_currencies()
13+
14+
col1, col2 = st.columns(2)
15+
with col1:
16+
from_currency = st.selectbox("From", currencies, index=currencies.index("USD"))
17+
with col2:
18+
to_currency = st.selectbox("To", currencies, index=currencies.index("EUR"))
19+
20+
amount = st.number_input("Amount", min_value=0.01, value=100.0, step=1.0)
21+
22+
if st.button("Convert"):
23+
result = ur.convert(to_currency, amount, from_currency)
24+
rate = ur.get_rate(from_currency, to_currency)
25+
st.metric(
26+
label=f"{amount} {from_currency} =",
27+
value=f"{result:,.4f} {to_currency}",
28+
delta=f"1 {from_currency} = {rate:.6f} {to_currency}",
29+
)

pyproject.toml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "streamlit-unirate"
7+
version = "0.1.0"
8+
description = "Streamlit integration for the UniRate currency-exchange API — cached helpers with st.cache_resource and st.cache_data support."
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
license = { text = "MIT" }
12+
authors = [{ name = "Unirate Team" }]
13+
keywords = [
14+
"streamlit",
15+
"unirate",
16+
"currency",
17+
"exchange-rates",
18+
"forex",
19+
"money",
20+
"fintech",
21+
"data",
22+
]
23+
classifiers = [
24+
"Development Status :: 4 - Beta",
25+
"Environment :: Web Environment",
26+
"Intended Audience :: Developers",
27+
"Intended Audience :: Science/Research",
28+
"License :: OSI Approved :: MIT License",
29+
"Operating System :: OS Independent",
30+
"Programming Language :: Python :: 3",
31+
"Programming Language :: Python :: 3.10",
32+
"Programming Language :: Python :: 3.11",
33+
"Programming Language :: Python :: 3.12",
34+
"Programming Language :: Python :: 3.13",
35+
"Topic :: Internet :: WWW/HTTP",
36+
"Topic :: Office/Business :: Financial",
37+
"Topic :: Software Development :: Libraries :: Python Modules",
38+
"Topic :: Scientific/Engineering",
39+
]
40+
dependencies = [
41+
"streamlit>=1.25,<2.0",
42+
"unirate-api>=1.0,<2.0",
43+
]
44+
45+
[project.urls]
46+
Homepage = "https://github.com/UniRate-API/streamlit-unirate"
47+
Repository = "https://github.com/UniRate-API/streamlit-unirate"
48+
Issues = "https://github.com/UniRate-API/streamlit-unirate/issues"
49+
Provider = "https://unirateapi.com"
50+
51+
[dependency-groups]
52+
test = [
53+
"pytest>=8.0,<9.0",
54+
]
55+
lint = [
56+
"ruff>=0.6,<1.0",
57+
]
58+
typing = [
59+
"mypy>=1.10,<2.0",
60+
"types-requests>=2.31",
61+
]
62+
63+
[tool.hatch.build.targets.wheel]
64+
packages = ["streamlit_unirate"]
65+
66+
[tool.ruff]
67+
target-version = "py310"
68+
line-length = 88
69+
70+
[tool.ruff.lint]
71+
select = ["E", "F", "I", "B", "UP", "RUF"]
72+
73+
[tool.mypy]
74+
python_version = "3.12"
75+
disallow_untyped_defs = true
76+
warn_redundant_casts = true
77+
warn_unused_ignores = true
78+
79+
[[tool.mypy.overrides]]
80+
module = ["unirate", "unirate.*", "streamlit", "streamlit.*"]
81+
ignore_missing_imports = true
82+
83+
[[tool.mypy.overrides]]
84+
module = ["numpy", "numpy.*"]
85+
follow_imports = "skip"
86+
87+
[[tool.mypy.overrides]]
88+
module = ["tests.*"]
89+
disallow_untyped_defs = false
90+
91+
[tool.pytest.ini_options]
92+
testpaths = ["tests"]
93+
addopts = "-ra"

streamlit_unirate/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Streamlit integration for the UniRate currency-exchange API."""
2+
3+
from streamlit_unirate._helpers import (
4+
convert,
5+
get_client,
6+
get_rate,
7+
get_vat_rates,
8+
list_currencies,
9+
)
10+
11+
__all__ = [
12+
"convert",
13+
"get_client",
14+
"get_rate",
15+
"get_vat_rates",
16+
"list_currencies",
17+
]
18+
19+
__version__ = "0.1.0"
423 Bytes
Binary file not shown.
3.7 KB
Binary file not shown.

0 commit comments

Comments
 (0)