avios-cli 0.4.2__tar.gz → 0.4.3__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.
Files changed (54) hide show
  1. {avios_cli-0.4.2 → avios_cli-0.4.3}/CHANGELOG.md +14 -1
  2. {avios_cli-0.4.2 → avios_cli-0.4.3}/PKG-INFO +16 -16
  3. {avios_cli-0.4.2 → avios_cli-0.4.3}/README.md +13 -13
  4. {avios_cli-0.4.2 → avios_cli-0.4.3}/pyproject.toml +8 -6
  5. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/__init__.py +1 -1
  6. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/auth.py +45 -19
  7. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/test_auth.py +41 -1
  8. {avios_cli-0.4.2 → avios_cli-0.4.3}/uv.lock +4 -8
  9. {avios_cli-0.4.2 → avios_cli-0.4.3}/.editorconfig +0 -0
  10. {avios_cli-0.4.2 → avios_cli-0.4.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {avios_cli-0.4.2 → avios_cli-0.4.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {avios_cli-0.4.2 → avios_cli-0.4.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {avios_cli-0.4.2 → avios_cli-0.4.3}/.github/dependabot.yml +0 -0
  14. {avios_cli-0.4.2 → avios_cli-0.4.3}/.github/workflows/ci.yml +0 -0
  15. {avios_cli-0.4.2 → avios_cli-0.4.3}/.github/workflows/publish.yml +0 -0
  16. {avios_cli-0.4.2 → avios_cli-0.4.3}/.gitignore +0 -0
  17. {avios_cli-0.4.2 → avios_cli-0.4.3}/.pre-commit-config.yaml +0 -0
  18. {avios_cli-0.4.2 → avios_cli-0.4.3}/.python-version +0 -0
  19. {avios_cli-0.4.2 → avios_cli-0.4.3}/CONTRIBUTING.md +0 -0
  20. {avios_cli-0.4.2 → avios_cli-0.4.3}/LICENSE +0 -0
  21. {avios_cli-0.4.2 → avios_cli-0.4.3}/SECURITY.md +0 -0
  22. {avios_cli-0.4.2 → avios_cli-0.4.3}/docs/dashboard.svg +0 -0
  23. {avios_cli-0.4.2 → avios_cli-0.4.3}/docs/reward-flights.svg +0 -0
  24. {avios_cli-0.4.2 → avios_cli-0.4.3}/scripts/screenshot.py +0 -0
  25. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/accounts.py +0 -0
  26. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/aggregate.py +0 -0
  27. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/cli.py +0 -0
  28. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/client.py +0 -0
  29. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/config.py +0 -0
  30. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/endpoints.py +0 -0
  31. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/finnair.py +0 -0
  32. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/models.py +0 -0
  33. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/programmes.py +0 -0
  34. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/rewards.py +0 -0
  35. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/session.py +0 -0
  36. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/tui/__init__.py +0 -0
  37. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/tui/app.py +0 -0
  38. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/tui/art.py +0 -0
  39. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/tui/rewards.py +0 -0
  40. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/tui/styles.tcss +0 -0
  41. {avios_cli-0.4.2 → avios_cli-0.4.3}/src/avios/tui/widgets.py +0 -0
  42. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/conftest.py +0 -0
  43. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/fixtures/balance.json +0 -0
  44. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/fixtures/reward_search.rsc +0 -0
  45. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/fixtures/transactions.json +0 -0
  46. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/test_accounts.py +0 -0
  47. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/test_aggregate.py +0 -0
  48. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/test_cli.py +0 -0
  49. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/test_client.py +0 -0
  50. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/test_finnair.py +0 -0
  51. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/test_models.py +0 -0
  52. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/test_rewards.py +0 -0
  53. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/test_session.py +0 -0
  54. {avios_cli-0.4.2 → avios_cli-0.4.3}/tests/test_tui.py +0 -0
@@ -6,6 +6,18 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.3] - 2026-07-27
10
+
11
+ ### Changed
12
+ - **`uvx avios-cli login` now works with no extra flags.** Browser-assisted login
13
+ (Playwright + browser-cookie3) moved from the optional `[login]` extra into the
14
+ base package, so `uvx avios-cli login iberia` just works — no more
15
+ `uvx --from 'avios-cli[login]' …`. The `login` extra is kept as a no-op alias
16
+ for backward compatibility.
17
+ - If you have no system Chrome, `avios login` now **downloads Playwright's Chromium
18
+ automatically** on first use (~150 MB, one-time) instead of failing with an
19
+ install hint.
20
+
9
21
  ## [0.4.2] - 2026-07-27
10
22
 
11
23
  ### Fixed
@@ -198,7 +210,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
198
210
  - Reward-flight **availability** search is not yet implemented (needs a British
199
211
  Airways capture).
200
212
 
201
- [Unreleased]: https://github.com/alexechoi/avios-cli/compare/v0.4.2...HEAD
213
+ [Unreleased]: https://github.com/alexechoi/avios-cli/compare/v0.4.3...HEAD
214
+ [0.4.3]: https://github.com/alexechoi/avios-cli/compare/v0.4.2...v0.4.3
202
215
  [0.4.2]: https://github.com/alexechoi/avios-cli/compare/v0.4.1...v0.4.2
203
216
  [0.4.1]: https://github.com/alexechoi/avios-cli/compare/v0.4.0...v0.4.1
204
217
  [0.4.0]: https://github.com/alexechoi/avios-cli/compare/v0.3.0...v0.4.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avios-cli
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: A CLI and TUI for Avios balances, transactions, and BA reward-flight availability.
5
5
  Project-URL: Homepage, https://github.com/alexechoi/avios-cli
6
6
  Project-URL: Repository, https://github.com/alexechoi/avios-cli
@@ -21,7 +21,9 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Topic :: Utilities
23
23
  Requires-Python: >=3.10
24
+ Requires-Dist: browser-cookie3>=0.19
24
25
  Requires-Dist: httpx>=0.27
26
+ Requires-Dist: playwright>=1.44
25
27
  Requires-Dist: pydantic-settings>=2.3
26
28
  Requires-Dist: pydantic>=2.7
27
29
  Requires-Dist: pyyaml>=6.0
@@ -29,8 +31,6 @@ Requires-Dist: rich>=13.7
29
31
  Requires-Dist: textual>=0.80
30
32
  Requires-Dist: typer>=0.12
31
33
  Provides-Extra: login
32
- Requires-Dist: browser-cookie3>=0.19; extra == 'login'
33
- Requires-Dist: playwright>=1.44; extra == 'login'
34
34
  Description-Content-Type: text/markdown
35
35
 
36
36
  # avios-cli
@@ -86,22 +86,26 @@ uv run avios --help
86
86
  avios.com has no credential API — login is Auth0 Universal Login behind hCaptcha and
87
87
  SMS/passkey MFA — so `avios login` opens a real browser, **waits while you finish
88
88
  logging in** (password, captcha, SMS code), and captures the session once you land
89
- on the dashboard. It needs the **`login` extra**:
89
+ on the dashboard:
90
90
 
91
91
  ```bash
92
- uvx --from 'avios-cli[login]' avios login
93
- uvx --from 'avios-cli[login]' avios login finnair
92
+ uvx avios-cli login
93
+ uvx avios-cli login iberia
94
+ uvx avios-cli login finnair
94
95
  ```
95
96
 
96
- It launches your installed **Google Chrome** (no extra download). If you don't have
97
- Chrome, install Playwright's browser once with
98
- `uvx --from playwright playwright install chromium`.
97
+ It launches your installed **Google Chrome** (no download). If you don't have
98
+ Chrome, it fetches Playwright's Chromium automatically on first login (~150 MB,
99
+ one-time). No extra flags — browser support ships in the package.
100
+
101
+ > Tip: if you'll use it often, `uv tool install avios-cli` once, then just run
102
+ > `avios login`, `avios balance`, etc. directly.
99
103
 
100
104
  Prefer not to open a browser? Import the cookie from a Chrome you're already logged
101
105
  into avios.com with:
102
106
 
103
107
  ```bash
104
- uvx --from 'avios-cli[login]' avios login --from-browser
108
+ uvx avios-cli login --from-browser
105
109
  ```
106
110
 
107
111
  This scans **all your Chrome profiles** and uses whichever one is actually logged
@@ -109,16 +113,12 @@ into avios.com (it tells you which). If you have several profiles, you can targe
109
113
  one directly:
110
114
 
111
115
  ```bash
112
- uvx --from 'avios-cli[login]' avios login --from-browser --profile "Profile 1"
116
+ uvx avios-cli login --from-browser --profile "Profile 1"
113
117
  ```
114
118
 
115
- Plain `uvx avios-cli login` won't work — the isolated environment doesn't include
116
- the `login` extra.
117
-
118
119
  **Stuck in an endless captcha loop?** That's bot detection on the automated
119
120
  browser. Use `--from-browser` instead: log into avios.com in your normal Chrome
120
- (you'll get one normal captcha), then run
121
- `uvx --from 'avios-cli[login]' avios login --from-browser`.
121
+ (you'll get one normal captcha), then run `uvx avios-cli login --from-browser`.
122
122
 
123
123
  Finnair uses a different CAS/OAuth flow. `avios login finnair` opens the Finnair Plus
124
124
  balance page, lets you complete password and MFA in the real browser, and captures the
@@ -51,22 +51,26 @@ uv run avios --help
51
51
  avios.com has no credential API — login is Auth0 Universal Login behind hCaptcha and
52
52
  SMS/passkey MFA — so `avios login` opens a real browser, **waits while you finish
53
53
  logging in** (password, captcha, SMS code), and captures the session once you land
54
- on the dashboard. It needs the **`login` extra**:
54
+ on the dashboard:
55
55
 
56
56
  ```bash
57
- uvx --from 'avios-cli[login]' avios login
58
- uvx --from 'avios-cli[login]' avios login finnair
57
+ uvx avios-cli login
58
+ uvx avios-cli login iberia
59
+ uvx avios-cli login finnair
59
60
  ```
60
61
 
61
- It launches your installed **Google Chrome** (no extra download). If you don't have
62
- Chrome, install Playwright's browser once with
63
- `uvx --from playwright playwright install chromium`.
62
+ It launches your installed **Google Chrome** (no download). If you don't have
63
+ Chrome, it fetches Playwright's Chromium automatically on first login (~150 MB,
64
+ one-time). No extra flags — browser support ships in the package.
65
+
66
+ > Tip: if you'll use it often, `uv tool install avios-cli` once, then just run
67
+ > `avios login`, `avios balance`, etc. directly.
64
68
 
65
69
  Prefer not to open a browser? Import the cookie from a Chrome you're already logged
66
70
  into avios.com with:
67
71
 
68
72
  ```bash
69
- uvx --from 'avios-cli[login]' avios login --from-browser
73
+ uvx avios-cli login --from-browser
70
74
  ```
71
75
 
72
76
  This scans **all your Chrome profiles** and uses whichever one is actually logged
@@ -74,16 +78,12 @@ into avios.com (it tells you which). If you have several profiles, you can targe
74
78
  one directly:
75
79
 
76
80
  ```bash
77
- uvx --from 'avios-cli[login]' avios login --from-browser --profile "Profile 1"
81
+ uvx avios-cli login --from-browser --profile "Profile 1"
78
82
  ```
79
83
 
80
- Plain `uvx avios-cli login` won't work — the isolated environment doesn't include
81
- the `login` extra.
82
-
83
84
  **Stuck in an endless captcha loop?** That's bot detection on the automated
84
85
  browser. Use `--from-browser` instead: log into avios.com in your normal Chrome
85
- (you'll get one normal captcha), then run
86
- `uvx --from 'avios-cli[login]' avios login --from-browser`.
86
+ (you'll get one normal captcha), then run `uvx avios-cli login --from-browser`.
87
87
 
88
88
  Finnair uses a different CAS/OAuth flow. `avios login finnair` opens the Finnair Plus
89
89
  balance page, lets you complete password and MFA in the real browser, and captures the
@@ -33,16 +33,18 @@ dependencies = [
33
33
  "pydantic>=2.7",
34
34
  "pydantic-settings>=2.3",
35
35
  "pyyaml>=6.0",
36
- ]
37
-
38
- [project.optional-dependencies]
39
- # Browser-assisted login and BA reward search. Kept optional so account-only
40
- # commands remain lightweight when a user does not need a browser.
41
- login = [
36
+ # Browser-assisted login ships in the base package so `uvx avios-cli login`
37
+ # works with no extra flags. The Chromium *binary* is still fetched on demand
38
+ # (only if you have no system Chrome) — see avios.auth._install_chromium.
42
39
  "playwright>=1.44",
43
40
  "browser-cookie3>=0.19",
44
41
  ]
45
42
 
43
+ [project.optional-dependencies]
44
+ # Retained for backward compatibility: the login dependencies now ship in the
45
+ # base package, so `avios-cli[login]` resolves to the same thing as `avios-cli`.
46
+ login = []
47
+
46
48
  # Dev tooling lives in a PEP 735 dependency-group (not an extra) so that `uv sync`
47
49
  # and `uv run` install it by default — an extra would be pruned by `uv run`.
48
50
  [dependency-groups]
@@ -3,4 +3,4 @@
3
3
  View your Avios balance, transactions and reward-flight availability from the terminal.
4
4
  """
5
5
 
6
- __version__ = "0.4.2"
6
+ __version__ = "0.4.3"
@@ -5,12 +5,12 @@ hCaptcha, Akamai Bot Manager and SMS/passkey MFA, so there is no way to POST a
5
5
  username/password over HTTP and obtain a session. Instead we let a real browser
6
6
  handle the login once and capture the resulting session cookie.
7
7
 
8
- Two strategies, both requiring the optional ``login`` extra
9
- (``pip install "avios-cli[login]"``):
8
+ Playwright and browser_cookie3 ship in the base package, so these work with a
9
+ plain ``avios`` install (no extra flags). Two strategies:
10
10
 
11
11
  - :func:`login_via_browser` — open a Playwright browser, the user logs in
12
- (password + captcha + MFA), and we grab the cookies. Also needs
13
- ``playwright install chromium``.
12
+ (password + captcha + MFA), and we grab the cookies. The Chromium binary is
13
+ fetched on demand only if you have no system Chrome (see :func:`_install_chromium`).
14
14
  - :func:`import_from_browser` — read the avios.com cookie straight out of a
15
15
  running browser via ``browser_cookie3`` (no popup) if already logged in there.
16
16
  """
@@ -177,11 +177,9 @@ def _import_sync_playwright() -> Any:
177
177
  from playwright.sync_api import sync_playwright
178
178
  except ImportError as exc:
179
179
  raise LoginError(
180
- "Playwright isn't installed. Log in using the 'login' extra:\n"
181
- " uvx --from 'avios-cli[login]' avios login\n"
182
- " (first time only: uvx --from playwright playwright install chromium)\n"
183
- "Or import the cookie from Chrome instead (no browser download):\n"
184
- " uvx --from 'avios-cli[login]' avios login --from-browser"
180
+ "Playwright isn't available in this environment. Reinstall avios-cli "
181
+ "(e.g. `uv tool install avios-cli`), or import the cookie from your "
182
+ "browser instead:\n avios login --from-browser"
185
183
  ) from exc
186
184
  return sync_playwright
187
185
 
@@ -236,31 +234,59 @@ def _open_login_context(
236
234
  # Chrome's native headless networking can reach BA's login page, but its
237
235
  # default ``HeadlessChrome`` UA is rejected. Keep the normal Chrome UA.
238
236
  launch_options["user_agent"] = user_agent
237
+
238
+ def launch(**extra: Any) -> Any:
239
+ return pw.chromium.launch_persistent_context(
240
+ user_data_dir,
241
+ headless=headless,
242
+ args=args,
243
+ ignore_default_args=ignore_default_args,
244
+ **launch_options,
245
+ **extra,
246
+ )
247
+
248
+ # Prefer the user's real Chrome, then bundled Chromium.
239
249
  for extra in ({"channel": "chrome"}, {}):
240
250
  try:
241
- return pw.chromium.launch_persistent_context(
242
- user_data_dir,
243
- headless=headless,
244
- args=args,
245
- ignore_default_args=ignore_default_args,
246
- **launch_options,
247
- **extra,
248
- )
251
+ return launch(**extra)
249
252
  except Exception:
250
253
  continue
254
+ # Neither is available (no system Chrome and no downloaded Chromium). Fetch
255
+ # the Chromium binary once, then retry the bundled browser.
256
+ if _install_chromium():
257
+ try:
258
+ return launch()
259
+ except Exception:
260
+ pass
251
261
  raise LoginError(
252
262
  "Couldn't launch a browser. Install Chromium once with:\n"
253
263
  " uvx --from playwright playwright install chromium"
254
264
  )
255
265
 
256
266
 
267
+ def _install_chromium() -> bool:
268
+ """Download Playwright's Chromium binary on demand. Returns True if it ran."""
269
+ import subprocess
270
+ import sys
271
+
272
+ print("No browser found — downloading Chromium (~150 MB, one-time)…", file=sys.stderr)
273
+ try:
274
+ subprocess.run(
275
+ [sys.executable, "-m", "playwright", "install", "chromium"],
276
+ check=True,
277
+ )
278
+ return True
279
+ except Exception:
280
+ return False
281
+
282
+
257
283
  def _bc3() -> Any:
258
284
  try:
259
285
  import browser_cookie3 as bc3
260
286
  except ImportError as exc:
261
287
  raise LoginError(
262
- "browser-cookie3 isn't installed. Run with the 'login' extra:\n"
263
- " uvx --from 'avios-cli[login]' avios login --from-browser"
288
+ "browser-cookie3 isn't available in this environment. "
289
+ "Reinstall avios-cli (e.g. `uv tool install avios-cli`)."
264
290
  ) from exc
265
291
  return bc3
266
292
 
@@ -101,7 +101,7 @@ def test_login_via_browser_without_playwright(
101
101
  ) -> None:
102
102
  monkeypatch.setitem(sys.modules, "playwright", None)
103
103
  monkeypatch.setitem(sys.modules, "playwright.sync_api", None)
104
- with pytest.raises(LoginError, match="Playwright isn't installed"):
104
+ with pytest.raises(LoginError, match="Playwright isn't available"):
105
105
  login_via_browser(BA, settings=settings)
106
106
 
107
107
 
@@ -245,3 +245,43 @@ def test_open_login_context_prefers_chrome_then_falls_back(tmp_path: object) ->
245
245
  result = _open_login_context(pw, headless=True, user_data_dir=str(tmp_path))
246
246
  assert result is ctx
247
247
  assert pw.chromium.channels == ["chrome", None] # tried Chrome, fell back to Chromium
248
+
249
+
250
+ def test_open_login_context_installs_chromium_when_missing(
251
+ tmp_path: object, monkeypatch: pytest.MonkeyPatch
252
+ ) -> None:
253
+ from avios import auth
254
+
255
+ ctx = _FakeContext(ok_after=1, cookies=[])
256
+
257
+ class _NoBrowserThenInstalled:
258
+ """No system Chrome and no Chromium until an install runs, then Chromium works."""
259
+
260
+ def __init__(self) -> None:
261
+ self.channels: list[str | None] = []
262
+ self.installed = False
263
+
264
+ def launch_persistent_context(self, user_data_dir: str, **kwargs: object) -> _FakeContext:
265
+ channel = kwargs.get("channel")
266
+ self.channels.append(channel) # type: ignore[arg-type]
267
+ if channel == "chrome":
268
+ raise RuntimeError("no system chrome")
269
+ if not self.installed: # bundled Chromium not present yet
270
+ raise RuntimeError("Executable doesn't exist")
271
+ return ctx
272
+
273
+ class _PW:
274
+ def __init__(self) -> None:
275
+ self.chromium = _NoBrowserThenInstalled()
276
+
277
+ pw = _PW()
278
+
279
+ def fake_install() -> bool:
280
+ pw.chromium.installed = True
281
+ return True
282
+
283
+ monkeypatch.setattr(auth, "_install_chromium", fake_install)
284
+ result = _open_login_context(pw, headless=True, user_data_dir=str(tmp_path))
285
+ assert result is ctx
286
+ # tried chrome, tried bundled (missing), then bundled again after install
287
+ assert pw.chromium.channels == ["chrome", None, None]
@@ -83,7 +83,9 @@ wheels = [
83
83
  name = "avios-cli"
84
84
  source = { editable = "." }
85
85
  dependencies = [
86
+ { name = "browser-cookie3" },
86
87
  { name = "httpx" },
88
+ { name = "playwright" },
87
89
  { name = "pydantic" },
88
90
  { name = "pydantic-settings" },
89
91
  { name = "pyyaml" },
@@ -92,12 +94,6 @@ dependencies = [
92
94
  { name = "typer" },
93
95
  ]
94
96
 
95
- [package.optional-dependencies]
96
- login = [
97
- { name = "browser-cookie3" },
98
- { name = "playwright" },
99
- ]
100
-
101
97
  [package.dev-dependencies]
102
98
  dev = [
103
99
  { name = "mypy" },
@@ -111,9 +107,9 @@ dev = [
111
107
 
112
108
  [package.metadata]
113
109
  requires-dist = [
114
- { name = "browser-cookie3", marker = "extra == 'login'", specifier = ">=0.19" },
110
+ { name = "browser-cookie3", specifier = ">=0.19" },
115
111
  { name = "httpx", specifier = ">=0.27" },
116
- { name = "playwright", marker = "extra == 'login'", specifier = ">=1.44" },
112
+ { name = "playwright", specifier = ">=1.44" },
117
113
  { name = "pydantic", specifier = ">=2.7" },
118
114
  { name = "pydantic-settings", specifier = ">=2.3" },
119
115
  { name = "pyyaml", specifier = ">=6.0" },
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