burnbox 1.0.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.
Files changed (52) hide show
  1. burnbox-1.0.0/.github/workflows/ci.yml +42 -0
  2. burnbox-1.0.0/.gitignore +13 -0
  3. burnbox-1.0.0/LICENSE +21 -0
  4. burnbox-1.0.0/PKG-INFO +31 -0
  5. burnbox-1.0.0/README.md +240 -0
  6. burnbox-1.0.0/burnbox/__init__.py +34 -0
  7. burnbox-1.0.0/burnbox/__main__.py +4 -0
  8. burnbox-1.0.0/burnbox/api.py +181 -0
  9. burnbox-1.0.0/burnbox/cli.py +349 -0
  10. burnbox-1.0.0/burnbox/client.py +67 -0
  11. burnbox-1.0.0/burnbox/config.py +120 -0
  12. burnbox-1.0.0/burnbox/detectors/__init__.py +31 -0
  13. burnbox-1.0.0/burnbox/detectors/base.py +28 -0
  14. burnbox-1.0.0/burnbox/detectors/clipboard.py +80 -0
  15. burnbox-1.0.0/burnbox/detectors/engine.py +70 -0
  16. burnbox-1.0.0/burnbox/detectors/i18n.py +142 -0
  17. burnbox-1.0.0/burnbox/detectors/parsers/__init__.py +13 -0
  18. burnbox-1.0.0/burnbox/detectors/parsers/alphanumeric_otp.py +85 -0
  19. burnbox-1.0.0/burnbox/detectors/parsers/labeled_otp.py +51 -0
  20. burnbox-1.0.0/burnbox/detectors/parsers/numeric_otp.py +74 -0
  21. burnbox-1.0.0/burnbox/detectors/parsers/reset_link.py +53 -0
  22. burnbox-1.0.0/burnbox/detectors/parsers/url_code.py +51 -0
  23. burnbox-1.0.0/burnbox/exceptions.py +31 -0
  24. burnbox-1.0.0/burnbox/models.py +36 -0
  25. burnbox-1.0.0/burnbox/notifications.py +53 -0
  26. burnbox-1.0.0/burnbox/providers/__init__.py +3 -0
  27. burnbox-1.0.0/burnbox/providers/base.py +46 -0
  28. burnbox-1.0.0/burnbox/providers/guerrillamail.py +164 -0
  29. burnbox-1.0.0/burnbox/providers/mailgw.py +20 -0
  30. burnbox-1.0.0/burnbox/providers/mailtm.py +237 -0
  31. burnbox-1.0.0/burnbox/providers/onesecmail.py +138 -0
  32. burnbox-1.0.0/burnbox/providers/registry.py +84 -0
  33. burnbox-1.0.0/burnbox/providers/sanitize.py +14 -0
  34. burnbox-1.0.0/burnbox/providers/utils.py +25 -0
  35. burnbox-1.0.0/burnbox/retry.py +105 -0
  36. burnbox-1.0.0/burnbox/session.py +79 -0
  37. burnbox-1.0.0/pyproject.toml +59 -0
  38. burnbox-1.0.0/tests/__init__.py +0 -0
  39. burnbox-1.0.0/tests/conftest.py +0 -0
  40. burnbox-1.0.0/tests/test_api.py +128 -0
  41. burnbox-1.0.0/tests/test_cli.py +55 -0
  42. burnbox-1.0.0/tests/test_client.py +132 -0
  43. burnbox-1.0.0/tests/test_config.py +58 -0
  44. burnbox-1.0.0/tests/test_detectors.py +352 -0
  45. burnbox-1.0.0/tests/test_integration.py +113 -0
  46. burnbox-1.0.0/tests/test_models.py +23 -0
  47. burnbox-1.0.0/tests/test_notifications.py +41 -0
  48. burnbox-1.0.0/tests/test_providers.py +395 -0
  49. burnbox-1.0.0/tests/test_registry.py +115 -0
  50. burnbox-1.0.0/tests/test_retry.py +134 -0
  51. burnbox-1.0.0/tests/test_session.py +100 -0
  52. burnbox-1.0.0/uv.lock +597 -0
@@ -0,0 +1,42 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ branches: [main, master]
9
+
10
+ jobs:
11
+ lint:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v4
16
+ - run: uv sync --extra dev
17
+ - run: uv run ruff check .
18
+ - run: uv run mypy burnbox/
19
+
20
+ test:
21
+ runs-on: ubuntu-latest
22
+ strategy:
23
+ matrix:
24
+ python-version: ["3.10", "3.12", "3.13"]
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v4
28
+ - run: uv sync --python ${{ matrix.python-version }} --extra dev
29
+ - run: uv run pytest -q
30
+
31
+ publish:
32
+ if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
33
+ needs: [lint, test]
34
+ runs-on: ubuntu-latest
35
+ environment: pypi
36
+ permissions:
37
+ id-token: write
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ - uses: astral-sh/setup-uv@v4
41
+ - run: uv build
42
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .mypy_cache/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .venv/
11
+ venv/
12
+ .env
13
+ *.so
burnbox-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 burnbox contributors
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.
burnbox-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: burnbox
3
+ Version: 1.0.0
4
+ Summary: Temporary email that burns after reading
5
+ Author: burnbox contributors
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: cli,disposable-email,otp,temp-mail,temporary-email
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Communications :: Email
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: html2text
22
+ Requires-Dist: httpx
23
+ Requires-Dist: pyperclip
24
+ Requires-Dist: rich
25
+ Requires-Dist: tomli; python_version < '3.11'
26
+ Requires-Dist: typer
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy; extra == 'dev'
29
+ Requires-Dist: pytest; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio; extra == 'dev'
31
+ Requires-Dist: ruff; extra == 'dev'
@@ -0,0 +1,240 @@
1
+ # burnbox
2
+
3
+ **Temporary email that burns after reading.**
4
+
5
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ burnbox creates a disposable email address, watches for incoming messages, auto-detects OTP codes, copies them to your clipboard — then burns the account when you're done.
9
+
10
+ Requires Python >= 3.10.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install burnbox
16
+ ```
17
+
18
+ Or with [pipx](https://pypa.github.io/pipx/) (recommended for CLI tools):
19
+
20
+ ```bash
21
+ pipx install burnbox
22
+ ```
23
+
24
+ ## How it works
25
+
26
+ 1. **Register** — burnbox creates a temporary email account
27
+ 2. **Poll** — watches for incoming messages every few seconds
28
+ 3. **Detect** — finds OTP codes, verification links, and copies them to clipboard
29
+ 4. **Burn** — deletes the account and session on exit (Ctrl+C)
30
+
31
+ The `--keep` flag preserves the account for later use with `burnbox resume`.
32
+
33
+ ## Quick start
34
+
35
+ ```bash
36
+ $ burnbox
37
+ ```
38
+
39
+ That's it. You'll get a temp address, it auto-copies to clipboard, and burnbox watches for messages. When a verification code arrives, it's detected and copied. Press Ctrl+C to exit — the account is deleted automatically.
40
+
41
+ ```
42
+ ╭─────────── burnbox ───────────╮
43
+ │ Temp email that burns after │
44
+ │ reading │
45
+ ╰───────────────────────────────╯
46
+
47
+ Provider: mailtm
48
+ Address: k7x9m2@example.com
49
+ Address copied to clipboard
50
+
51
+ Ctrl+C to exit and burn · --keep to preserve · burnbox resume
52
+ ```
53
+
54
+ ## CLI usage
55
+
56
+ | Command | Description |
57
+ |---|---|
58
+ | `burnbox` | Create temp email, watch for messages, burn on exit |
59
+ | `burnbox address` | Generate a temp email address and exit (account is burned immediately) |
60
+ | `burnbox resume` | Reconnect to the last saved session |
61
+
62
+ ### Options
63
+
64
+ ```
65
+ --provider Provider: mailtm, mailgw, 1secmail, guerrillamail
66
+ --poll, -p Polling interval in seconds (default: 5)
67
+ --timeout, -t HTTP request timeout (default: 10)
68
+ --keep, -k Keep account alive after exit
69
+ --version, -v Show version
70
+ --help, -h Show help
71
+ ```
72
+
73
+ ### Examples
74
+
75
+ ```bash
76
+ # Use a specific provider
77
+ burnbox --provider guerrillamail
78
+
79
+ # Keep account alive for later resume
80
+ burnbox --keep
81
+
82
+ # Resume a kept session
83
+ burnbox resume
84
+
85
+ # One-shot: just get a temp address (burned immediately)
86
+ burnbox address
87
+ ```
88
+
89
+ ## Programmatic API
90
+
91
+ Use burnbox as a Python library:
92
+
93
+ ```python
94
+ import asyncio
95
+ import burnbox
96
+
97
+ async def main():
98
+ box = await burnbox.create()
99
+ async with box:
100
+ print(f"Address: {box.address}")
101
+ msg = await box.wait_for_message(timeout=60)
102
+ if msg:
103
+ print(f"Code: {msg.best_code}")
104
+ print(f"From: {msg.sender}")
105
+ # Account auto-burns on exit
106
+
107
+ asyncio.run(main())
108
+ ```
109
+
110
+ ### API reference
111
+
112
+ - `burnbox.create(provider=None, config=None)` — Create a `BurnBox` instance (await it first, then use as context manager)
113
+ - `box.address` — The temp email address
114
+ - `box.fetch_new()` — Fetch new messages
115
+ - `box.wait_for_message(timeout=60)` — Wait for the first message (returns `None` on timeout)
116
+ - `box.messages()` — Async iterator yielding messages as they arrive
117
+ - `box.burn()` — Delete the account manually
118
+ - `msg.id`, `msg.sender`, `msg.subject` — Message metadata
119
+ - `msg.content` — Message body as plain text
120
+ - `msg.best_code` — Highest-confidence OTP code detected (string or `None`)
121
+ - `msg.codes` — All detected codes as `CodeMatch` objects (`.value`, `.confidence`, `.kind`)
122
+ - `msg.links` — All detected links
123
+
124
+ ## Configuration
125
+
126
+ Config file: `~/.config/burnbox.toml`
127
+
128
+ ```toml
129
+ [provider]
130
+ default = "mailtm" # Preferred provider
131
+ custom_url = "https://..." # Custom API URL for mailtm/mailgw
132
+
133
+ [polling]
134
+ interval = 5.0 # Seconds between polls
135
+ timeout = 10.0 # HTTP timeout
136
+
137
+ [output]
138
+ copy_address = true # Copy address to clipboard
139
+ copy_code = true # Copy OTP codes to clipboard
140
+ notifications = true # Desktop notifications on OTP
141
+ ```
142
+
143
+ Environment variables override the config:
144
+
145
+ ```bash
146
+ BURNBOX_PROVIDER=guerrillamail
147
+ BURNBOX_POLL_INTERVAL=3
148
+ BURNBOX_TIMEOUT=15
149
+ BURNBOX_CUSTOM_URL=https://...
150
+ ```
151
+
152
+ ## Providers
153
+
154
+ | Provider | Auth | Delete account | Domains | Custom URL |
155
+ |---|---|---|---|---|
156
+ | **mail.tm** | Register + token | Yes | Multiple | Yes |
157
+ | **mail.gw** | Register + token | Yes | Multiple | Yes |
158
+ | **1secmail** | None (stateless) | Auto-expire | Dynamic (via API) | No |
159
+ | **guerrillamail** | Session-based | Yes | sharklasers.com, grr.la, etc | No |
160
+
161
+ burnbox automatically selects the first available provider with a health check. If one is down, it falls back to the next.
162
+
163
+ ## OTP Detection
164
+
165
+ burnbox detects verification codes from incoming emails using a multi-parser engine:
166
+
167
+ - **Labeled OTP** — "code: 1234", "Your verification code: 8472", "Ваш код: 5531", etc.
168
+ - **Alphanumeric codes** — Recovery codes, backup keys (e.g., `A1B2-C3D4-E5F6`)
169
+ - **URL-embedded codes** — `?code=`, `?token=`, `?otp=` in links
170
+ - **Reset/verify links** — Password reset, account verification URLs
171
+ - **Numeric OTP** — Standalone digit clusters with context-aware confidence boosting
172
+
173
+ Supports 12 languages for label detection: English, Russian, German, French, Spanish, Portuguese, Chinese, Japanese, Korean, Hindi, Arabic, Turkish. Context-aware confidence boosting is available for English and Russian; other languages use label-matching only.
174
+
175
+ Each match has a **confidence score** (0–1). The highest-confidence code is auto-copied to your clipboard.
176
+
177
+ ## Plugin system
178
+
179
+ Add custom providers via Python entry points:
180
+
181
+ ```python
182
+ # my_provider.py
183
+ from burnbox.providers.base import Provider
184
+ from burnbox.models import Session, InboxMessage
185
+
186
+ class MyProvider:
187
+ name = "myprovider"
188
+ supports_custom_url = False
189
+
190
+ async def is_alive(self) -> bool:
191
+ # Check if the provider API is reachable
192
+
193
+ async def register(self) -> Session:
194
+ # Create a new temp email account, return Session
195
+
196
+ async def restore(self, session: Session) -> None:
197
+ # Restore auth state from a saved session
198
+
199
+ async def fetch_messages(self, seen_ids: set[str]) -> list[InboxMessage]:
200
+ # Fetch new (unseen) messages
201
+
202
+ async def delete_account(self, account_id: str) -> bool:
203
+ # Delete the account, return True on success
204
+
205
+ async def aclose(self) -> None:
206
+ # Close the HTTP client
207
+ ```
208
+
209
+ ```toml
210
+ # pyproject.toml
211
+ [project.entry-points."burnbox.providers"]
212
+ myprovider = "my_provider:MyProvider"
213
+ ```
214
+
215
+ After `pip install`, burnbox discovers your provider automatically.
216
+
217
+ ## Security considerations
218
+
219
+ - OTP codes transit through third-party email providers. Only use burnbox for non-sensitive verifications.
220
+ - Session files are stored with 0600 permissions at `~/.config/burnbox/session.json`.
221
+ - Accounts are deleted ("burned") on exit by default. Use `--keep` only if you need persistence.
222
+
223
+ ## Troubleshooting
224
+
225
+ - **Clipboard not working on Linux**: Install `xclip` or `xsel` (X11) or `wl-clipboard` (Wayland).
226
+ - **All providers down**: burnbox falls back to trying providers even if health checks fail. Check your network.
227
+ - **"Session expired" on resume**: The temp email account has expired. Start a new one with `burnbox`.
228
+
229
+ ## Development
230
+
231
+ ```bash
232
+ uv sync --extra dev
233
+ uv run ruff check .
234
+ uv run mypy burnbox/
235
+ uv run pytest
236
+ ```
237
+
238
+ ## License
239
+
240
+ MIT
@@ -0,0 +1,34 @@
1
+ from burnbox.api import BurnBox, Message, create
2
+ from burnbox.client import BurnBoxClient
3
+ from burnbox.config import AppConfig, load_config
4
+ from burnbox.exceptions import (
5
+ APIError,
6
+ AuthExpiredError,
7
+ BurnBoxError,
8
+ NoDomainsError,
9
+ ProviderError,
10
+ SessionError,
11
+ TokenError,
12
+ )
13
+ from burnbox.models import InboxMessage, MessagePreview, Session
14
+
15
+ __version__ = "1.0.0"
16
+
17
+ __all__ = [
18
+ "BurnBox",
19
+ "BurnBoxClient",
20
+ "Message",
21
+ "AppConfig",
22
+ "load_config",
23
+ "create",
24
+ "BurnBoxError",
25
+ "Session",
26
+ "InboxMessage",
27
+ "MessagePreview",
28
+ "APIError",
29
+ "NoDomainsError",
30
+ "ProviderError",
31
+ "SessionError",
32
+ "TokenError",
33
+ "AuthExpiredError",
34
+ ]
@@ -0,0 +1,4 @@
1
+ from burnbox.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import replace
5
+ from typing import AsyncIterator
6
+
7
+ from burnbox.client import BurnBoxClient
8
+ from burnbox.config import AppConfig, load_config
9
+ from burnbox.detectors.base import CodeMatch, MessageContext
10
+ from burnbox.detectors.engine import ParserEngine
11
+ from burnbox.exceptions import AuthExpiredError, BurnBoxError
12
+ from burnbox.models import InboxMessage, Session
13
+ from burnbox.providers.base import Provider
14
+ from burnbox.providers.registry import select_provider
15
+ from burnbox.providers.utils import build_registry
16
+ from burnbox.session import SessionStore
17
+
18
+
19
+ async def _select(config: AppConfig) -> Provider:
20
+ registry = build_registry(config.custom_url)
21
+ all_providers = registry.all()
22
+ provider = await select_provider(all_providers, preferred=config.provider_default)
23
+ if not provider:
24
+ for p in all_providers:
25
+ try:
26
+ await p.aclose()
27
+ except Exception:
28
+ pass
29
+ raise RuntimeError("No available providers. Check your network.")
30
+ for p in all_providers:
31
+ if p is not provider:
32
+ try:
33
+ await p.aclose()
34
+ except Exception:
35
+ pass
36
+ return provider
37
+
38
+
39
+ class Message:
40
+ def __init__(self, inner: InboxMessage, engine: ParserEngine) -> None:
41
+ self._inner = inner
42
+ self._engine = engine
43
+ self._codes: list[CodeMatch] | None = None
44
+
45
+ @property
46
+ def id(self) -> str:
47
+ return self._inner.id
48
+
49
+ @property
50
+ def sender(self) -> str:
51
+ return self._inner.sender
52
+
53
+ @property
54
+ def subject(self) -> str:
55
+ return self._inner.subject
56
+
57
+ @property
58
+ def content(self) -> str:
59
+ return self._inner.content
60
+
61
+ @property
62
+ def codes(self) -> list[CodeMatch]:
63
+ if self._codes is None:
64
+ ctx = MessageContext(sender=self.sender, subject=self.subject)
65
+ self._codes = self._engine.parse(self.content, ctx)
66
+ return self._codes
67
+
68
+ @property
69
+ def best_code(self) -> str | None:
70
+ best = self._engine.best_code(self.codes)
71
+ return best.value if best else None
72
+
73
+ @property
74
+ def links(self) -> list[str]:
75
+ return self._engine.detect_links(self.content)
76
+
77
+
78
+ class BurnBox:
79
+ """High-level async interface for temporary email.
80
+
81
+ Usage::
82
+
83
+ async with burnbox.create() as box:
84
+ print(box.address)
85
+ msg = await box.wait_for_message(timeout=60)
86
+ if msg:
87
+ print(msg.best_code)
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ provider: Provider,
93
+ client: BurnBoxClient,
94
+ config: AppConfig,
95
+ engine: ParserEngine | None = None,
96
+ ) -> None:
97
+ self._provider = provider
98
+ self._client = client
99
+ self._config = config
100
+ self._engine = engine or ParserEngine()
101
+ self._seen_ids: set[str] = set()
102
+
103
+ @property
104
+ def address(self) -> str | None:
105
+ s = self._client.session
106
+ return s.address if s else None
107
+
108
+ @property
109
+ def session(self) -> Session | None:
110
+ return self._client.session
111
+
112
+ async def fetch_new(self) -> list[Message]:
113
+ raw = await self._client.fetch_new(self._seen_ids)
114
+ messages = [Message(m, self._engine) for m in raw]
115
+ for m in raw:
116
+ self._seen_ids.add(m.id)
117
+ return messages
118
+
119
+ async def wait_for_message(self, timeout: float = 60.0) -> Message | None:
120
+ loop = asyncio.get_running_loop()
121
+ deadline = loop.time() + timeout
122
+ while True:
123
+ messages = await self.fetch_new()
124
+ if messages:
125
+ return messages[0]
126
+ remaining = deadline - loop.time()
127
+ if remaining <= 0:
128
+ return None
129
+ await asyncio.sleep(min(self._config.poll_interval, remaining))
130
+
131
+ _MAX_CONSECUTIVE_ERRORS = 5
132
+
133
+ async def messages(self, poll_interval: float | None = None) -> AsyncIterator[Message]:
134
+ interval = poll_interval or self._config.poll_interval
135
+ consecutive_errors = 0
136
+ while True:
137
+ try:
138
+ new = await self.fetch_new()
139
+ consecutive_errors = 0
140
+ for m in new:
141
+ yield m
142
+ except (AuthExpiredError, BurnBoxError):
143
+ raise
144
+ except Exception as exc:
145
+ consecutive_errors += 1
146
+ if consecutive_errors >= self._MAX_CONSECUTIVE_ERRORS:
147
+ raise BurnBoxError(
148
+ f"Too many consecutive errors ({consecutive_errors}). Last: {exc}"
149
+ ) from exc
150
+ await asyncio.sleep(interval)
151
+
152
+ async def burn(self) -> bool:
153
+ return await self._client.burn()
154
+
155
+ async def __aenter__(self) -> BurnBox:
156
+ return self
157
+
158
+ async def __aexit__(self, *args: object) -> None:
159
+ try:
160
+ await self.burn()
161
+ except Exception:
162
+ pass
163
+ try:
164
+ await self._provider.aclose()
165
+ except Exception:
166
+ pass
167
+
168
+
169
+ async def create(
170
+ provider: str | None = None,
171
+ config: AppConfig | None = None,
172
+ ) -> BurnBox:
173
+ cfg = config or load_config()
174
+ if provider:
175
+ cfg = replace(cfg, provider_default=provider)
176
+
177
+ prov = await _select(cfg)
178
+ store = SessionStore()
179
+ client = BurnBoxClient(provider=prov, session_store=store, config=cfg)
180
+ await client.register()
181
+ return BurnBox(provider=prov, client=client, config=cfg)