avios-cli 0.1.6__tar.gz → 0.2.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.
- {avios_cli-0.1.6 → avios_cli-0.2.0}/CHANGELOG.md +15 -1
- {avios_cli-0.1.6 → avios_cli-0.2.0}/PKG-INFO +6 -11
- {avios_cli-0.1.6 → avios_cli-0.2.0}/README.md +5 -10
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/__init__.py +1 -1
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/cli.py +27 -23
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/client.py +9 -4
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/config.py +3 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/models.py +18 -6
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/session.py +2 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/tui/app.py +8 -7
- avios_cli-0.2.0/tests/fixtures/transactions.json +25 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_cli.py +20 -5
- {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_client.py +21 -29
- {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_models.py +21 -2
- {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_session.py +1 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_tui.py +7 -3
- avios_cli-0.1.6/tests/fixtures/transactions.json +0 -5
- {avios_cli-0.1.6 → avios_cli-0.2.0}/.editorconfig +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/dependabot.yml +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/workflows/ci.yml +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/.github/workflows/publish.yml +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/.gitignore +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/.pre-commit-config.yaml +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/.python-version +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/CONTRIBUTING.md +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/LICENSE +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/SECURITY.md +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/docs/dashboard.svg +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/pyproject.toml +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/scripts/screenshot.py +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/auth.py +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/endpoints.py +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/tui/__init__.py +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/tui/art.py +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/tui/styles.tcss +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/src/avios/tui/widgets.py +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/conftest.py +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/fixtures/balance.json +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/tests/test_auth.py +0 -0
- {avios_cli-0.1.6 → avios_cli-0.2.0}/uv.lock +0 -0
|
@@ -6,6 +6,19 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-07-24
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **`transactions`, `pending` and `overview` now work.** The manage-avios API
|
|
13
|
+
only needed an `x-avios-opco` header (not a separate session, as previously
|
|
14
|
+
thought) — it's now sent automatically. `transactions` renders date /
|
|
15
|
+
description / Avios (coloured +/-) / type and respects `--limit`; the TUI shows
|
|
16
|
+
real transactions too.
|
|
17
|
+
- Typed `Transaction` model (identifier, dateProcessed, description, type,
|
|
18
|
+
partner, amount, categories) and a `NamedRef` helper.
|
|
19
|
+
- `AVIOS_OPCO` setting (default `BAEC` = British Airways Executive Club) for other
|
|
20
|
+
Avios programmes.
|
|
21
|
+
|
|
9
22
|
## [0.1.6] - 2026-07-24
|
|
10
23
|
|
|
11
24
|
### Fixed
|
|
@@ -107,7 +120,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
107
120
|
- Reward-flight **availability** search is not yet implemented (needs a British
|
|
108
121
|
Airways capture).
|
|
109
122
|
|
|
110
|
-
[Unreleased]: https://github.com/alexechoi/avios-cli/compare/v0.
|
|
123
|
+
[Unreleased]: https://github.com/alexechoi/avios-cli/compare/v0.2.0...HEAD
|
|
124
|
+
[0.2.0]: https://github.com/alexechoi/avios-cli/compare/v0.1.6...v0.2.0
|
|
111
125
|
[0.1.6]: https://github.com/alexechoi/avios-cli/compare/v0.1.5...v0.1.6
|
|
112
126
|
[0.1.5]: https://github.com/alexechoi/avios-cli/compare/v0.1.4...v0.1.5
|
|
113
127
|
[0.1.4]: https://github.com/alexechoi/avios-cli/compare/v0.1.3...v0.1.4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: avios-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A CLI and TUI for avios.com — view your Avios balance and transactions from the terminal.
|
|
5
5
|
Project-URL: Homepage, https://github.com/alexechoi/avios-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/alexechoi/avios-cli
|
|
@@ -120,20 +120,15 @@ expires after ~a day; just run `avios login` again. Run `avios logout` to remove
|
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
122
|
avios balance # total / individual / household Avios
|
|
123
|
+
avios transactions --limit 20 # recent transactions (date, description, Avios, type)
|
|
124
|
+
avios pending # pending Avios
|
|
125
|
+
avios overview # dashboard summary
|
|
123
126
|
avios whoami # name, tier, membership, email
|
|
124
127
|
avios raw /shell/api/users/current/accounts # hit any endpoint directly
|
|
125
128
|
```
|
|
126
129
|
|
|
127
|
-
Add `--json` to `balance` or `whoami` for scriptable
|
|
128
|
-
|
|
129
|
-
**Experimental** (need the `manage-avios` app session, which the cookie/SSO login
|
|
130
|
-
can't yet establish — these currently return "session expired"):
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
avios transactions --limit 20
|
|
134
|
-
avios pending
|
|
135
|
-
avios overview
|
|
136
|
-
```
|
|
130
|
+
Add `--json` to `balance`, `transactions`, `pending` or `whoami` for scriptable
|
|
131
|
+
output. Non-BA Avios programmes can set `AVIOS_OPCO` (default `BAEC`).
|
|
137
132
|
|
|
138
133
|
## TUI
|
|
139
134
|
|
|
@@ -85,20 +85,15 @@ expires after ~a day; just run `avios login` again. Run `avios logout` to remove
|
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
87
|
avios balance # total / individual / household Avios
|
|
88
|
+
avios transactions --limit 20 # recent transactions (date, description, Avios, type)
|
|
89
|
+
avios pending # pending Avios
|
|
90
|
+
avios overview # dashboard summary
|
|
88
91
|
avios whoami # name, tier, membership, email
|
|
89
92
|
avios raw /shell/api/users/current/accounts # hit any endpoint directly
|
|
90
93
|
```
|
|
91
94
|
|
|
92
|
-
Add `--json` to `balance` or `whoami` for scriptable
|
|
93
|
-
|
|
94
|
-
**Experimental** (need the `manage-avios` app session, which the cookie/SSO login
|
|
95
|
-
can't yet establish — these currently return "session expired"):
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
avios transactions --limit 20
|
|
99
|
-
avios pending
|
|
100
|
-
avios overview
|
|
101
|
-
```
|
|
95
|
+
Add `--json` to `balance`, `transactions`, `pending` or `whoami` for scriptable
|
|
96
|
+
output. Non-BA Avios programmes can set `AVIOS_OPCO` (default `BAEC`).
|
|
102
97
|
|
|
103
98
|
## TUI
|
|
104
99
|
|
|
@@ -20,7 +20,7 @@ from rich.table import Table
|
|
|
20
20
|
from avios import __version__
|
|
21
21
|
from avios.auth import LoginError, import_from_browser, login_via_browser
|
|
22
22
|
from avios.client import AviosClient
|
|
23
|
-
from avios.models import Balance
|
|
23
|
+
from avios.models import Balance, Transaction
|
|
24
24
|
from avios.session import NotAuthenticated, Session, SessionExpired
|
|
25
25
|
|
|
26
26
|
app = typer.Typer(
|
|
@@ -78,19 +78,6 @@ def _print_json(data: Any) -> None:
|
|
|
78
78
|
console.print_json(data=data)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
def _render_records(records: list[dict[str, Any]], title: str) -> None:
|
|
82
|
-
if not records:
|
|
83
|
-
console.print(f"[dim]No {title.lower()}.[/]")
|
|
84
|
-
return
|
|
85
|
-
keys = [k for k, v in records[0].items() if not isinstance(v, dict | list)][:6]
|
|
86
|
-
table = Table(title=title)
|
|
87
|
-
for key in keys:
|
|
88
|
-
table.add_column(key)
|
|
89
|
-
for record in records:
|
|
90
|
-
table.add_row(*[str(record.get(key, "")) for key in keys])
|
|
91
|
-
console.print(table)
|
|
92
|
-
|
|
93
|
-
|
|
94
81
|
# -- auth commands -----------------------------------------------------------
|
|
95
82
|
@app.command()
|
|
96
83
|
def login(
|
|
@@ -172,36 +159,53 @@ def _render_balance(result: Balance) -> None:
|
|
|
172
159
|
console.print(table)
|
|
173
160
|
|
|
174
161
|
|
|
162
|
+
def _render_transactions(items: list[Transaction], title: str) -> None:
|
|
163
|
+
if not items:
|
|
164
|
+
console.print(f"[dim]No {title.lower()}.[/]")
|
|
165
|
+
return
|
|
166
|
+
table = Table(title=title)
|
|
167
|
+
table.add_column("Date")
|
|
168
|
+
table.add_column("Description")
|
|
169
|
+
table.add_column("Avios", justify="right")
|
|
170
|
+
table.add_column("Type")
|
|
171
|
+
for txn in items:
|
|
172
|
+
date = (txn.date_processed or "")[:10]
|
|
173
|
+
desc = (txn.description or "").splitlines()[0][:44] if txn.description else ""
|
|
174
|
+
amount = f"{txn.amount:+,}" if txn.amount is not None else ""
|
|
175
|
+
colour = "green" if (txn.amount or 0) >= 0 else "red"
|
|
176
|
+
kind = txn.type.value if txn.type else ""
|
|
177
|
+
table.add_row(date, desc, f"[{colour}]{amount}[/]", kind or "")
|
|
178
|
+
console.print(table)
|
|
179
|
+
|
|
180
|
+
|
|
175
181
|
@app.command()
|
|
176
182
|
def transactions(
|
|
177
183
|
limit: int = typer.Option(20, help="Number of transactions to show."),
|
|
178
184
|
json_out: bool = JSON_OPTION,
|
|
179
185
|
) -> None:
|
|
180
|
-
"""List recent Avios transactions
|
|
186
|
+
"""List recent Avios transactions."""
|
|
181
187
|
with _handle_errors():
|
|
182
188
|
items = _client().get_transactions(limit=limit)
|
|
183
|
-
records = [item.as_dict() for item in items]
|
|
184
189
|
if json_out:
|
|
185
|
-
_print_json(
|
|
190
|
+
_print_json([item.as_dict() for item in items])
|
|
186
191
|
return
|
|
187
|
-
|
|
192
|
+
_render_transactions(items, "Transactions")
|
|
188
193
|
|
|
189
194
|
|
|
190
195
|
@app.command()
|
|
191
196
|
def pending(json_out: bool = JSON_OPTION) -> None:
|
|
192
|
-
"""List pending Avios transactions
|
|
197
|
+
"""List pending Avios transactions."""
|
|
193
198
|
with _handle_errors():
|
|
194
199
|
items = _client().get_pending_transactions()
|
|
195
|
-
records = [item.as_dict() for item in items]
|
|
196
200
|
if json_out:
|
|
197
|
-
_print_json(
|
|
201
|
+
_print_json([item.as_dict() for item in items])
|
|
198
202
|
return
|
|
199
|
-
|
|
203
|
+
_render_transactions(items, "Pending")
|
|
200
204
|
|
|
201
205
|
|
|
202
206
|
@app.command()
|
|
203
207
|
def overview() -> None:
|
|
204
|
-
"""Show the dashboard overview (
|
|
208
|
+
"""Show the dashboard overview (raw JSON)."""
|
|
205
209
|
with _handle_errors():
|
|
206
210
|
result = _client().get_overview()
|
|
207
211
|
_print_json(result.as_dict())
|
|
@@ -47,11 +47,16 @@ class AviosClient:
|
|
|
47
47
|
def get_overview(self) -> Overview:
|
|
48
48
|
return Overview.model_validate(self.session.get_json(endpoints.OVERVIEW))
|
|
49
49
|
|
|
50
|
-
def get_transactions(self, limit: int
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
def get_transactions(self, limit: int = 50) -> list[Transaction]:
|
|
51
|
+
# `offset` is a server-side page hint; slice to `limit` for an exact count.
|
|
52
|
+
offset = limit if limit and limit > 0 else 1000
|
|
53
|
+
path = f"{endpoints.TRANSACTIONS}?startRecord=1&offset={offset}&status=completed"
|
|
54
|
+
payload = self.session.get_json(path)
|
|
55
|
+
items = (
|
|
56
|
+
payload.get("transactions", []) if isinstance(payload, dict) else _extract_list(payload)
|
|
57
|
+
)
|
|
53
58
|
transactions = [Transaction.model_validate(item) for item in items]
|
|
54
|
-
return transactions[:limit] if limit
|
|
59
|
+
return transactions[:limit] if limit and limit > 0 else transactions
|
|
55
60
|
|
|
56
61
|
def get_pending_transactions(self) -> list[Transaction]:
|
|
57
62
|
payload = self.session.get_json(endpoints.TRANSACTIONS_PENDING)
|
|
@@ -39,6 +39,9 @@ class Settings(BaseSettings):
|
|
|
39
39
|
user_agent: str = DEFAULT_USER_AGENT
|
|
40
40
|
config_dir: Path = Field(default_factory=_default_config_dir)
|
|
41
41
|
request_timeout: float = 20.0
|
|
42
|
+
# Operating company code sent as `x-avios-opco`; required by the manage-avios
|
|
43
|
+
# API. "BAEC" = British Airways Executive Club (override with AVIOS_OPCO).
|
|
44
|
+
opco: str = "BAEC"
|
|
42
45
|
|
|
43
46
|
@property
|
|
44
47
|
def state_path(self) -> Path:
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
-
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class AviosModel(BaseModel):
|
|
@@ -35,12 +35,24 @@ class Balance(AviosModel):
|
|
|
35
35
|
household: int | None = None
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
class
|
|
39
|
-
"""A
|
|
38
|
+
class NamedRef(AviosModel):
|
|
39
|
+
"""A ``{value, id}`` reference (transaction type, partner, ...)."""
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
value: str | None = None
|
|
42
|
+
id: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Transaction(AviosModel):
|
|
46
|
+
"""A single Avios statement entry (from the manage-avios transactions API)."""
|
|
47
|
+
|
|
48
|
+
identifier: str | None = None
|
|
49
|
+
date_processed: str | None = Field(default=None, alias="dateProcessed")
|
|
50
|
+
description: str | None = None
|
|
51
|
+
type: NamedRef | None = None
|
|
52
|
+
partner: NamedRef | None = None
|
|
53
|
+
amount: int | None = None
|
|
54
|
+
categories: list[str] = Field(default_factory=list)
|
|
55
|
+
record_index: int | None = Field(default=None, alias="recordIndex")
|
|
44
56
|
|
|
45
57
|
|
|
46
58
|
class Account(AviosModel):
|
|
@@ -95,6 +95,8 @@ class Session:
|
|
|
95
95
|
"accept": "application/json, text/plain, */*",
|
|
96
96
|
"user-agent": self.settings.user_agent,
|
|
97
97
|
"referer": f"{self.settings.base_url}/manage-avios/dashboard",
|
|
98
|
+
# Required by the manage-avios API (401 without it).
|
|
99
|
+
"x-avios-opco": self.settings.opco,
|
|
98
100
|
"cookie": cookie,
|
|
99
101
|
}
|
|
100
102
|
return httpx.Client(
|
|
@@ -21,7 +21,6 @@ from avios.tui.art import banner_text
|
|
|
21
21
|
from avios.tui.widgets import BalanceDisplay
|
|
22
22
|
|
|
23
23
|
TRANSACTIONS_TO_SHOW = 50
|
|
24
|
-
MAX_COLUMNS = 6
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
class AviosApp(App[None]):
|
|
@@ -87,15 +86,17 @@ class AviosApp(App[None]):
|
|
|
87
86
|
table = self.query_one("#transactions", DataTable)
|
|
88
87
|
table.loading = False
|
|
89
88
|
table.clear(columns=True)
|
|
90
|
-
|
|
91
|
-
if not records:
|
|
89
|
+
if not transactions:
|
|
92
90
|
table.add_column("info")
|
|
93
91
|
table.add_row("No transactions")
|
|
94
92
|
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
table.add_columns("Date", "Description", "Avios", "Type")
|
|
94
|
+
for txn in transactions:
|
|
95
|
+
date = (txn.date_processed or "")[:10]
|
|
96
|
+
desc = (txn.description or "").splitlines()[0][:44] if txn.description else ""
|
|
97
|
+
amount = f"{txn.amount:+,}" if txn.amount is not None else ""
|
|
98
|
+
kind = txn.type.value if txn.type else ""
|
|
99
|
+
table.add_row(date, desc, amount, kind)
|
|
99
100
|
|
|
100
101
|
|
|
101
102
|
def run(client: AviosClient | None = None) -> None:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"totalRecords": 2,
|
|
3
|
+
"transactions": [
|
|
4
|
+
{
|
|
5
|
+
"identifier": "2139440378",
|
|
6
|
+
"dateProcessed": "2026-07-21T11:37:35.000Z",
|
|
7
|
+
"description": "UBER",
|
|
8
|
+
"type": {"value": "Collection", "id": "DV_COL"},
|
|
9
|
+
"partner": {"value": "UBER", "id": "UBR"},
|
|
10
|
+
"amount": 50,
|
|
11
|
+
"categories": ["collectingAvios"],
|
|
12
|
+
"recordIndex": 1
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"identifier": "2023965591",
|
|
16
|
+
"dateProcessed": "2025-12-10T21:37:04.000Z",
|
|
17
|
+
"description": "Tokyo Haneda to London Heathrow\nClass: Club World\nBA0008",
|
|
18
|
+
"type": {"value": "Reward Flight", "id": "FLT_RWD"},
|
|
19
|
+
"partner": {"value": "British Airways", "id": "BA"},
|
|
20
|
+
"amount": -52000,
|
|
21
|
+
"categories": ["flights"],
|
|
22
|
+
"recordIndex": 2
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -30,12 +30,26 @@ class FakeClient:
|
|
|
30
30
|
def get_balance(self) -> Balance:
|
|
31
31
|
return Balance(balance=100, individual=100, household=200)
|
|
32
32
|
|
|
33
|
-
def get_transactions(self, limit: int
|
|
33
|
+
def get_transactions(self, limit: int = 50) -> list[Transaction]:
|
|
34
34
|
items = [
|
|
35
|
-
Transaction.model_validate(
|
|
36
|
-
|
|
35
|
+
Transaction.model_validate(
|
|
36
|
+
{
|
|
37
|
+
"dateProcessed": "2026-07-01T09:00:00.000Z",
|
|
38
|
+
"description": "UBER",
|
|
39
|
+
"type": {"value": "Collection", "id": "DV_COL"},
|
|
40
|
+
"amount": 100,
|
|
41
|
+
}
|
|
42
|
+
),
|
|
43
|
+
Transaction.model_validate(
|
|
44
|
+
{
|
|
45
|
+
"dateProcessed": "2026-06-01T09:00:00.000Z",
|
|
46
|
+
"description": "Deliveroo UK",
|
|
47
|
+
"type": {"value": "BA Avios eStore", "id": "BA_AV_ESTORE"},
|
|
48
|
+
"amount": 50,
|
|
49
|
+
}
|
|
50
|
+
),
|
|
37
51
|
]
|
|
38
|
-
return items[:limit]
|
|
52
|
+
return items[:limit]
|
|
39
53
|
|
|
40
54
|
def get_pending_transactions(self) -> list[Transaction]:
|
|
41
55
|
return []
|
|
@@ -100,7 +114,8 @@ def test_transactions_respects_limit(fake_client: None) -> None:
|
|
|
100
114
|
def test_transactions_json(fake_client: None) -> None:
|
|
101
115
|
result = runner.invoke(app, ["transactions", "--json"])
|
|
102
116
|
assert result.exit_code == 0
|
|
103
|
-
assert '"
|
|
117
|
+
assert '"amount"' in result.stdout
|
|
118
|
+
assert '"dateProcessed"' in result.stdout
|
|
104
119
|
|
|
105
120
|
|
|
106
121
|
def test_whoami(fake_client: None) -> None:
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
"""Tests for the typed API client.
|
|
2
|
-
|
|
3
|
-
The transactions/accounts fixtures are representative, not confirmed shapes, so
|
|
4
|
-
these tests assert on counts and field pass-through (via ``extra='allow'``) rather
|
|
5
|
-
than a rigid schema. The balance shape is confirmed.
|
|
6
|
-
"""
|
|
1
|
+
"""Tests for the typed API client (balance, transactions, profile, ...)."""
|
|
7
2
|
|
|
8
3
|
from __future__ import annotations
|
|
9
4
|
|
|
@@ -19,8 +14,12 @@ from avios.config import Settings
|
|
|
19
14
|
from avios.session import Session, SessionExpired
|
|
20
15
|
|
|
21
16
|
|
|
22
|
-
def _client(
|
|
17
|
+
def _client(
|
|
18
|
+
settings: Settings, routes: dict[str, Any], captured: list[httpx.Request] | None = None
|
|
19
|
+
) -> AviosClient:
|
|
23
20
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
21
|
+
if captured is not None:
|
|
22
|
+
captured.append(request)
|
|
24
23
|
if request.url.path not in routes:
|
|
25
24
|
return httpx.Response(404, json={})
|
|
26
25
|
body = routes[request.url.path]
|
|
@@ -40,28 +39,21 @@ def test_get_balance(settings: Settings, load_fixture: Callable[[str], Any]) ->
|
|
|
40
39
|
assert balance.household == 112430
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
def
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
assert txns
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
assert
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_get_transactions_from_wrapped_object(settings: Settings) -> None:
|
|
60
|
-
payload = {"total": 1, "transactions": [{"date": "2026-07-01", "avios": 100}]}
|
|
61
|
-
client = _client(settings, {endpoints.TRANSACTIONS: payload})
|
|
62
|
-
txns = client.get_transactions()
|
|
63
|
-
assert len(txns) == 1
|
|
64
|
-
assert txns[0].as_dict()["avios"] == 100
|
|
42
|
+
def test_get_transactions(settings: Settings, load_fixture: Callable[[str], Any]) -> None:
|
|
43
|
+
captured: list[httpx.Request] = []
|
|
44
|
+
client = _client(
|
|
45
|
+
settings, {endpoints.TRANSACTIONS: load_fixture("transactions.json")}, captured
|
|
46
|
+
)
|
|
47
|
+
txns = client.get_transactions(limit=2)
|
|
48
|
+
assert len(txns) == 2
|
|
49
|
+
assert txns[0].type is not None and txns[0].type.value == "Collection"
|
|
50
|
+
assert txns[0].amount == 50
|
|
51
|
+
assert txns[1].amount == -52000
|
|
52
|
+
# Sends the required opco header and the limit as offset.
|
|
53
|
+
req = captured[-1]
|
|
54
|
+
assert req.headers["x-avios-opco"] == "BAEC"
|
|
55
|
+
assert "offset=2" in str(req.url)
|
|
56
|
+
assert "status=completed" in str(req.url)
|
|
65
57
|
|
|
66
58
|
|
|
67
59
|
def test_get_profile(settings: Settings) -> None:
|
|
@@ -29,6 +29,25 @@ def test_balance_extras_are_optional() -> None:
|
|
|
29
29
|
def test_extra_fields_are_preserved() -> None:
|
|
30
30
|
txn = Transaction.model_validate({"date": "2026-07-01", "avios": 500, "unknown": "x"})
|
|
31
31
|
dumped = txn.as_dict()
|
|
32
|
-
assert dumped["date"] == "2026-07-01"
|
|
33
|
-
assert dumped["avios"] == 500
|
|
34
32
|
assert dumped["unknown"] == "x"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_transaction_parses_real_shape() -> None:
|
|
36
|
+
txn = Transaction.model_validate(
|
|
37
|
+
{
|
|
38
|
+
"identifier": "2139440378",
|
|
39
|
+
"dateProcessed": "2026-07-21T11:37:35.000Z",
|
|
40
|
+
"description": "UBER",
|
|
41
|
+
"type": {"value": "Collection", "id": "DV_COL"},
|
|
42
|
+
"partner": {"value": "UBER", "id": "UBR"},
|
|
43
|
+
"amount": 50,
|
|
44
|
+
"categories": ["collectingAvios"],
|
|
45
|
+
"recordIndex": 1,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
assert txn.identifier == "2139440378"
|
|
49
|
+
assert txn.date_processed == "2026-07-21T11:37:35.000Z"
|
|
50
|
+
assert txn.type is not None and txn.type.value == "Collection"
|
|
51
|
+
assert txn.partner is not None and txn.partner.id == "UBR"
|
|
52
|
+
assert txn.amount == 50
|
|
53
|
+
assert txn.categories == ["collectingAvios"]
|
|
@@ -67,6 +67,7 @@ def test_client_requires_authentication(settings: Settings) -> None:
|
|
|
67
67
|
def test_get_json_returns_parsed_body(settings: Settings) -> None:
|
|
68
68
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
69
69
|
assert request.headers["cookie"] == "appSession=abc"
|
|
70
|
+
assert request.headers["x-avios-opco"] == "BAEC"
|
|
70
71
|
return httpx.Response(200, json={"balance": 75751})
|
|
71
72
|
|
|
72
73
|
session = _session(settings, httpx.MockTransport(handler))
|
|
@@ -17,10 +17,14 @@ class FakeClient:
|
|
|
17
17
|
def get_balance(self) -> Balance:
|
|
18
18
|
return Balance(balance=100, individual=100, household=200)
|
|
19
19
|
|
|
20
|
-
def get_transactions(self, limit: int
|
|
20
|
+
def get_transactions(self, limit: int = 50) -> list[Transaction]:
|
|
21
21
|
return [
|
|
22
|
-
Transaction.model_validate(
|
|
23
|
-
|
|
22
|
+
Transaction.model_validate(
|
|
23
|
+
{"dateProcessed": "2026-07-01T09:00:00Z", "description": "UBER", "amount": 100}
|
|
24
|
+
),
|
|
25
|
+
Transaction.model_validate(
|
|
26
|
+
{"dateProcessed": "2026-06-01T09:00:00Z", "description": "Flight", "amount": -5000}
|
|
27
|
+
),
|
|
24
28
|
]
|
|
25
29
|
|
|
26
30
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|