hawkapi-mail 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,52 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ name: Lint
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v4
15
+ with:
16
+ enable-cache: true
17
+ - name: Install dependencies
18
+ run: uv sync --extra dev
19
+ - name: ruff check
20
+ run: uv run ruff check .
21
+ - name: ruff format check
22
+ run: uv run ruff format --check .
23
+
24
+ typecheck:
25
+ name: Typecheck
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: astral-sh/setup-uv@v4
30
+ with:
31
+ enable-cache: true
32
+ - name: Install dependencies
33
+ run: uv sync --extra dev
34
+ - name: pyright
35
+ run: uv run pyright src/
36
+
37
+ test:
38
+ name: Test (Python ${{ matrix.python-version }})
39
+ runs-on: ubuntu-latest
40
+ strategy:
41
+ matrix:
42
+ python-version: ["3.12", "3.13"]
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ - uses: astral-sh/setup-uv@v4
46
+ with:
47
+ enable-cache: true
48
+ python-version: ${{ matrix.python-version }}
49
+ - name: Install dependencies
50
+ run: uv sync --extra dev
51
+ - name: Run tests
52
+ run: uv run pytest tests/ -q
@@ -0,0 +1,25 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ name: Build and publish to PyPI
10
+ runs-on: ubuntu-latest
11
+ environment: release
12
+ permissions:
13
+ id-token: write # required for trusted publishing
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v4
18
+ with:
19
+ enable-cache: true
20
+ - name: Build package
21
+ run: uv build
22
+ - name: Publish to PyPI
23
+ uses: pypa/gh-action-pypi-publish@release/v1
24
+ with:
25
+ packages-dir: dist/
@@ -0,0 +1,35 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ *.egg
9
+ .eggs/
10
+ .venv/
11
+ venv/
12
+ env/
13
+ .env
14
+ *.log
15
+ .mypy_cache/
16
+ .pyright/
17
+ .ruff_cache/
18
+ .pytest_cache/
19
+ htmlcov/
20
+ .coverage
21
+ .coverage.*
22
+ coverage.xml
23
+ *.cover
24
+ .hypothesis/
25
+ .tox/
26
+ .nox/
27
+ *.swp
28
+ *.swo
29
+ *~
30
+ .DS_Store
31
+ .idea/
32
+ .vscode/
33
+ .history/
34
+ site/
35
+ .remember/
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-05-16
4
+
5
+ Initial release.
6
+
7
+ - SMTP backend via aiosmtplib (TLS / STARTTLS / SSL).
8
+ - AWS SES backend (raw send via boto3, extras: `[ses]`).
9
+ - SendGrid v3, Mailgun v3, Resend HTTP backends.
10
+ - In-memory backend for tests.
11
+ - `EmailMessage` builder — text + HTML + attachments, to/cc/bcc/reply-to, tags, metadata.
12
+ - Jinja2 `TemplateRenderer` with async rendering + HTML autoescape.
13
+ - Persistent outbox (`MemoryOutbox`, `SQLiteOutbox`) with retry worker + exponential backoff.
14
+ - Webhook helpers: signature verification (Mailgun, Resend/Svix, SendGrid ECDSA) and event parsing (Mailgun, SendGrid, Resend, SES via SNS) normalized to `WebhookEvent`.
15
+ - `init_mail(app, ...)` + `Depends(get_mailer)`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HawkAPI 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.
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.4
2
+ Name: hawkapi-mail
3
+ Version: 0.1.0
4
+ Summary: Email plugin for HawkAPI — SMTP, SES, SendGrid, Mailgun, Resend, Jinja2, webhooks, persistent outbox
5
+ Project-URL: Homepage, https://pypi.org/project/hawkapi-mail/
6
+ Project-URL: Repository, https://github.com/ashimov/hawkapi-mail
7
+ Project-URL: Issues, https://github.com/ashimov/hawkapi-mail/issues
8
+ Author-email: HawkAPI Contributors <hawkapi@users.noreply.github.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 HawkAPI Contributors
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: email,hawkapi,jinja2,mail,mailgun,resend,sendgrid,ses,smtp
32
+ Classifier: Development Status :: 5 - Production/Stable
33
+ Classifier: Framework :: AsyncIO
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Topic :: Communications :: Email
40
+ Classifier: Topic :: Internet :: WWW/HTTP
41
+ Classifier: Typing :: Typed
42
+ Requires-Python: >=3.12
43
+ Requires-Dist: aiosmtplib>=3.0
44
+ Requires-Dist: aiosqlite>=0.20
45
+ Requires-Dist: hawkapi>=0.1.7
46
+ Requires-Dist: httpx>=0.27
47
+ Requires-Dist: jinja2>=3.1
48
+ Provides-Extra: dev
49
+ Requires-Dist: boto3>=1.34; extra == 'dev'
50
+ Requires-Dist: pyright>=1.1; extra == 'dev'
51
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
52
+ Requires-Dist: pytest>=8.0; extra == 'dev'
53
+ Requires-Dist: ruff>=0.8; extra == 'dev'
54
+ Provides-Extra: ses
55
+ Requires-Dist: boto3>=1.34; extra == 'ses'
56
+ Description-Content-Type: text/markdown
57
+
58
+ # hawkapi-mail
59
+
60
+ Email plugin for [HawkAPI](https://github.com/ashimov/HawkAPI). SMTP, AWS SES, SendGrid, Mailgun, Resend, Jinja2 templates, persistent outbox with retry, and webhook handlers for delivery/bounce/complaint events.
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pip install hawkapi-mail # SMTP + SendGrid + Mailgun + Resend + outbox
66
+ pip install 'hawkapi-mail[ses]' # adds AWS SES backend
67
+ ```
68
+
69
+ ## Quickstart
70
+
71
+ ```python
72
+ from hawkapi import Depends, HawkAPI
73
+ from hawkapi_mail import (
74
+ EmailMessage,
75
+ Mailer,
76
+ SMTPBackend,
77
+ SMTPConfig,
78
+ get_mailer,
79
+ init_mail,
80
+ )
81
+
82
+ app = HawkAPI()
83
+ init_mail(
84
+ app,
85
+ backend=SMTPBackend(SMTPConfig(host="smtp.example.com", port=587, start_tls=True,
86
+ username="api", password="secret")),
87
+ default_sender="hello@example.com",
88
+ )
89
+
90
+
91
+ @app.post("/welcome")
92
+ async def welcome(email: str, mail: Mailer = Depends(get_mailer)):
93
+ msg = EmailMessage.build(
94
+ subject="Welcome!", to=email, text="Glad you joined.",
95
+ html="<h1>Glad you joined.</h1>",
96
+ )
97
+ await mail.send(msg)
98
+ return {"ok": True}
99
+ ```
100
+
101
+ ## Backends
102
+
103
+ ```python
104
+ from hawkapi_mail import (
105
+ InMemoryBackend, # tests
106
+ SMTPBackend, SMTPConfig, # SMTP / SMTPS / STARTTLS
107
+ SESBackend, SESConfig, # AWS SES (extras: [ses])
108
+ SendGridBackend, SendGridConfig, # SendGrid v3 API
109
+ MailgunBackend, MailgunConfig, # Mailgun v3 API
110
+ ResendBackend, ResendConfig, # Resend HTTP API
111
+ )
112
+
113
+ sendgrid = SendGridBackend(SendGridConfig(api_key="SG.xxx"))
114
+ mailgun = MailgunBackend(MailgunConfig(api_key="key-xxx", domain="mg.example.com"))
115
+ resend = ResendBackend(ResendConfig(api_key="re_xxx"))
116
+ ses = SESBackend(SESConfig(region="eu-west-1")) # uses boto3 / IAM
117
+ ```
118
+
119
+ All backends share one async `send(message) -> SendResult` interface; swap them freely.
120
+
121
+ ## Templates
122
+
123
+ ```python
124
+ from hawkapi_mail import TemplateRenderer
125
+
126
+ templates = TemplateRenderer(directory="emails/") # or package=..., or templates={...}
127
+ init_mail(app, backend=..., templates=templates, default_sender="hello@example.com")
128
+
129
+ await mail.send_template(
130
+ "welcome.html",
131
+ text_template="welcome.txt",
132
+ context={"name": "Alice"},
133
+ subject="Welcome",
134
+ to="alice@example.com",
135
+ )
136
+ ```
137
+
138
+ Jinja2 with async rendering + HTML autoescape on by default.
139
+
140
+ ## Persistent outbox
141
+
142
+ ```python
143
+ from hawkapi_mail import SQLiteOutbox, RetryPolicy
144
+
145
+ outbox = SQLiteOutbox(path="mail.db")
146
+ init_mail(
147
+ app,
148
+ backend=sendgrid,
149
+ outbox=outbox,
150
+ retry=RetryPolicy(max_attempts=5, base_seconds=5, max_seconds=3600),
151
+ start_worker=True, # drains the outbox in the background
152
+ )
153
+
154
+ # Enqueue instead of sending right away:
155
+ entry_id = await mail.send(message, deferred=True)
156
+ ```
157
+
158
+ The worker pulls due entries, calls the backend, and on `SendError` schedules an exponential-backoff retry. After `max_attempts` the entry is dropped (logged at error level). For tests, swap in `MemoryOutbox()`.
159
+
160
+ ## Webhooks
161
+
162
+ ```python
163
+ from hawkapi_mail import (
164
+ verify_sendgrid, parse_sendgrid,
165
+ verify_mailgun, parse_mailgun,
166
+ verify_resend, parse_resend,
167
+ parse_ses_sns, confirm_ses_subscription,
168
+ )
169
+
170
+
171
+ @app.post("/webhooks/mailgun")
172
+ async def mailgun_hook(request):
173
+ form = await request.form()
174
+ verify_mailgun(
175
+ signing_key="…",
176
+ timestamp=form["timestamp"], token=form["token"], signature=form["signature"],
177
+ )
178
+ event = parse_mailgun(await request.body())
179
+ # event.kind ∈ delivered | bounce | complaint | opened | clicked | unsubscribed | other
180
+ return {"ok": True}
181
+
182
+
183
+ @app.post("/webhooks/ses")
184
+ async def ses_hook(request):
185
+ body = await request.body()
186
+ if await confirm_ses_subscription(body): # SNS SubscriptionConfirmation
187
+ return {"confirmed": True}
188
+ for event in parse_ses_sns(body):
189
+ ...
190
+ return {"ok": True}
191
+ ```
192
+
193
+ All providers normalize to a single `WebhookEvent(provider, kind, recipient, message_id, timestamp, raw)`.
194
+
195
+ ## Testing
196
+
197
+ ```python
198
+ from hawkapi_mail import InMemoryBackend, init_mail
199
+
200
+ backend = InMemoryBackend()
201
+ init_mail(app, backend=backend, default_sender="me@x.com")
202
+
203
+ # After exercising the app:
204
+ assert len(backend.sent) == 1
205
+ assert backend.sent[0].subject == "Welcome"
206
+ backend.clear()
207
+ ```
208
+
209
+ ## Development
210
+
211
+ ```bash
212
+ git clone https://github.com/ashimov/hawkapi-mail.git
213
+ cd hawkapi-mail
214
+ uv sync --extra dev
215
+ uv run pytest -q
216
+ uv run ruff check . && uv run ruff format --check .
217
+ uv run pyright src/
218
+ ```
219
+
220
+ ## License
221
+
222
+ MIT.
@@ -0,0 +1,165 @@
1
+ # hawkapi-mail
2
+
3
+ Email plugin for [HawkAPI](https://github.com/ashimov/HawkAPI). SMTP, AWS SES, SendGrid, Mailgun, Resend, Jinja2 templates, persistent outbox with retry, and webhook handlers for delivery/bounce/complaint events.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install hawkapi-mail # SMTP + SendGrid + Mailgun + Resend + outbox
9
+ pip install 'hawkapi-mail[ses]' # adds AWS SES backend
10
+ ```
11
+
12
+ ## Quickstart
13
+
14
+ ```python
15
+ from hawkapi import Depends, HawkAPI
16
+ from hawkapi_mail import (
17
+ EmailMessage,
18
+ Mailer,
19
+ SMTPBackend,
20
+ SMTPConfig,
21
+ get_mailer,
22
+ init_mail,
23
+ )
24
+
25
+ app = HawkAPI()
26
+ init_mail(
27
+ app,
28
+ backend=SMTPBackend(SMTPConfig(host="smtp.example.com", port=587, start_tls=True,
29
+ username="api", password="secret")),
30
+ default_sender="hello@example.com",
31
+ )
32
+
33
+
34
+ @app.post("/welcome")
35
+ async def welcome(email: str, mail: Mailer = Depends(get_mailer)):
36
+ msg = EmailMessage.build(
37
+ subject="Welcome!", to=email, text="Glad you joined.",
38
+ html="<h1>Glad you joined.</h1>",
39
+ )
40
+ await mail.send(msg)
41
+ return {"ok": True}
42
+ ```
43
+
44
+ ## Backends
45
+
46
+ ```python
47
+ from hawkapi_mail import (
48
+ InMemoryBackend, # tests
49
+ SMTPBackend, SMTPConfig, # SMTP / SMTPS / STARTTLS
50
+ SESBackend, SESConfig, # AWS SES (extras: [ses])
51
+ SendGridBackend, SendGridConfig, # SendGrid v3 API
52
+ MailgunBackend, MailgunConfig, # Mailgun v3 API
53
+ ResendBackend, ResendConfig, # Resend HTTP API
54
+ )
55
+
56
+ sendgrid = SendGridBackend(SendGridConfig(api_key="SG.xxx"))
57
+ mailgun = MailgunBackend(MailgunConfig(api_key="key-xxx", domain="mg.example.com"))
58
+ resend = ResendBackend(ResendConfig(api_key="re_xxx"))
59
+ ses = SESBackend(SESConfig(region="eu-west-1")) # uses boto3 / IAM
60
+ ```
61
+
62
+ All backends share one async `send(message) -> SendResult` interface; swap them freely.
63
+
64
+ ## Templates
65
+
66
+ ```python
67
+ from hawkapi_mail import TemplateRenderer
68
+
69
+ templates = TemplateRenderer(directory="emails/") # or package=..., or templates={...}
70
+ init_mail(app, backend=..., templates=templates, default_sender="hello@example.com")
71
+
72
+ await mail.send_template(
73
+ "welcome.html",
74
+ text_template="welcome.txt",
75
+ context={"name": "Alice"},
76
+ subject="Welcome",
77
+ to="alice@example.com",
78
+ )
79
+ ```
80
+
81
+ Jinja2 with async rendering + HTML autoescape on by default.
82
+
83
+ ## Persistent outbox
84
+
85
+ ```python
86
+ from hawkapi_mail import SQLiteOutbox, RetryPolicy
87
+
88
+ outbox = SQLiteOutbox(path="mail.db")
89
+ init_mail(
90
+ app,
91
+ backend=sendgrid,
92
+ outbox=outbox,
93
+ retry=RetryPolicy(max_attempts=5, base_seconds=5, max_seconds=3600),
94
+ start_worker=True, # drains the outbox in the background
95
+ )
96
+
97
+ # Enqueue instead of sending right away:
98
+ entry_id = await mail.send(message, deferred=True)
99
+ ```
100
+
101
+ The worker pulls due entries, calls the backend, and on `SendError` schedules an exponential-backoff retry. After `max_attempts` the entry is dropped (logged at error level). For tests, swap in `MemoryOutbox()`.
102
+
103
+ ## Webhooks
104
+
105
+ ```python
106
+ from hawkapi_mail import (
107
+ verify_sendgrid, parse_sendgrid,
108
+ verify_mailgun, parse_mailgun,
109
+ verify_resend, parse_resend,
110
+ parse_ses_sns, confirm_ses_subscription,
111
+ )
112
+
113
+
114
+ @app.post("/webhooks/mailgun")
115
+ async def mailgun_hook(request):
116
+ form = await request.form()
117
+ verify_mailgun(
118
+ signing_key="…",
119
+ timestamp=form["timestamp"], token=form["token"], signature=form["signature"],
120
+ )
121
+ event = parse_mailgun(await request.body())
122
+ # event.kind ∈ delivered | bounce | complaint | opened | clicked | unsubscribed | other
123
+ return {"ok": True}
124
+
125
+
126
+ @app.post("/webhooks/ses")
127
+ async def ses_hook(request):
128
+ body = await request.body()
129
+ if await confirm_ses_subscription(body): # SNS SubscriptionConfirmation
130
+ return {"confirmed": True}
131
+ for event in parse_ses_sns(body):
132
+ ...
133
+ return {"ok": True}
134
+ ```
135
+
136
+ All providers normalize to a single `WebhookEvent(provider, kind, recipient, message_id, timestamp, raw)`.
137
+
138
+ ## Testing
139
+
140
+ ```python
141
+ from hawkapi_mail import InMemoryBackend, init_mail
142
+
143
+ backend = InMemoryBackend()
144
+ init_mail(app, backend=backend, default_sender="me@x.com")
145
+
146
+ # After exercising the app:
147
+ assert len(backend.sent) == 1
148
+ assert backend.sent[0].subject == "Welcome"
149
+ backend.clear()
150
+ ```
151
+
152
+ ## Development
153
+
154
+ ```bash
155
+ git clone https://github.com/ashimov/hawkapi-mail.git
156
+ cd hawkapi-mail
157
+ uv sync --extra dev
158
+ uv run pytest -q
159
+ uv run ruff check . && uv run ruff format --check .
160
+ uv run pyright src/
161
+ ```
162
+
163
+ ## License
164
+
165
+ MIT.
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hawkapi-mail"
7
+ version = "0.1.0"
8
+ description = "Email plugin for HawkAPI — SMTP, SES, SendGrid, Mailgun, Resend, Jinja2, webhooks, persistent outbox"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.12"
12
+ authors = [
13
+ { name = "HawkAPI Contributors", email = "hawkapi@users.noreply.github.com" },
14
+ ]
15
+ keywords = ["hawkapi", "email", "mail", "smtp", "ses", "sendgrid", "mailgun", "resend", "jinja2"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Framework :: AsyncIO",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Communications :: Email",
25
+ "Topic :: Internet :: WWW/HTTP",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "hawkapi>=0.1.7",
30
+ "aiosmtplib>=3.0",
31
+ "httpx>=0.27",
32
+ "jinja2>=3.1",
33
+ "aiosqlite>=0.20",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ ses = ["boto3>=1.34"]
38
+ dev = [
39
+ "pytest>=8.0",
40
+ "pytest-asyncio>=0.24",
41
+ "boto3>=1.34",
42
+ "ruff>=0.8",
43
+ "pyright>=1.1",
44
+ ]
45
+
46
+ [project.urls]
47
+ Homepage = "https://pypi.org/project/hawkapi-mail/"
48
+ Repository = "https://github.com/ashimov/hawkapi-mail"
49
+ Issues = "https://github.com/ashimov/hawkapi-mail/issues"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/hawkapi_mail"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ asyncio_mode = "auto"
57
+ filterwarnings = ["ignore::DeprecationWarning"]
58
+
59
+ [tool.ruff]
60
+ target-version = "py312"
61
+ line-length = 100
62
+
63
+ [tool.ruff.lint]
64
+ select = ["E", "F", "I", "UP", "B", "SIM", "S"]
65
+ ignore = ["S101", "B008"]
66
+
67
+ [tool.ruff.lint.per-file-ignores]
68
+ "tests/**" = ["S"]
69
+
70
+ [tool.pyright]
71
+ pythonVersion = "3.12"
72
+ typeCheckingMode = "strict"
73
+ reportUnknownVariableType = false
74
+ reportUnknownMemberType = false
75
+ reportUnknownArgumentType = false
76
+ reportMissingTypeStubs = false