quillet 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.
Files changed (39) hide show
  1. quillet-0.1.0/.cursor/plans/flask_newsletter_package_e8a6fd1a.plan.md +298 -0
  2. quillet-0.1.0/.cursor/rules/project-rules.mdc +41 -0
  3. quillet-0.1.0/.cursor/rules/python.mdc +12 -0
  4. quillet-0.1.0/.github/workflows/release.yml +74 -0
  5. quillet-0.1.0/.github/workflows/test.yml +28 -0
  6. quillet-0.1.0/.gitignore +14 -0
  7. quillet-0.1.0/Dockerfile +22 -0
  8. quillet-0.1.0/LICENSE +21 -0
  9. quillet-0.1.0/PKG-INFO +322 -0
  10. quillet-0.1.0/README.md +286 -0
  11. quillet-0.1.0/docker-compose.yml +30 -0
  12. quillet-0.1.0/llm_runbook.md +506 -0
  13. quillet-0.1.0/pyproject.toml +48 -0
  14. quillet-0.1.0/quillet/__init__.py +22 -0
  15. quillet-0.1.0/quillet/_app.py +5 -0
  16. quillet-0.1.0/quillet/admin.py +176 -0
  17. quillet-0.1.0/quillet/auth.py +36 -0
  18. quillet-0.1.0/quillet/cli.py +126 -0
  19. quillet-0.1.0/quillet/db/__init__.py +33 -0
  20. quillet-0.1.0/quillet/db/sqlalchemy.py +246 -0
  21. quillet-0.1.0/quillet/db/supabase_rest.py +228 -0
  22. quillet-0.1.0/quillet/email/__init__.py +20 -0
  23. quillet-0.1.0/quillet/email/mailgun.py +91 -0
  24. quillet-0.1.0/quillet/email/noop.py +39 -0
  25. quillet-0.1.0/quillet/email/smtp.py +108 -0
  26. quillet-0.1.0/quillet/factory.py +153 -0
  27. quillet-0.1.0/quillet/models.py +29 -0
  28. quillet-0.1.0/quillet/routes.py +254 -0
  29. quillet-0.1.0/quillet/templates/quillet/admin/dashboard.html +76 -0
  30. quillet-0.1.0/quillet/templates/quillet/admin/post_form.html +99 -0
  31. quillet-0.1.0/quillet/templates/quillet/admin/subscribers.html +55 -0
  32. quillet-0.1.0/quillet/templates/quillet/base.html +34 -0
  33. quillet-0.1.0/quillet/templates/quillet/post_detail.html +20 -0
  34. quillet-0.1.0/quillet/templates/quillet/post_list.html +33 -0
  35. quillet-0.1.0/quillet/templates/quillet/subscribe_confirm.html +29 -0
  36. quillet-0.1.0/scripts/docker_entrypoint_test.sh +67 -0
  37. quillet-0.1.0/scripts/docker_test.sh +41 -0
  38. quillet-0.1.0/scripts/http_smoke_test.py +217 -0
  39. quillet-0.1.0/scripts/smoke_test.py +148 -0
@@ -0,0 +1,298 @@
1
+ ---
2
+ name: Quillet
3
+ overview: Design and build QUILLET — a standalone, open-source Flask newsletter/microblog package that can be used as an importable Blueprint, a standalone Docker web app, or a headless API — with pluggable DB and email backends.
4
+ todos:
5
+ - id: repo-scaffold
6
+ content: "Create new repo: pyproject.toml, Dockerfile, docker-compose.yml, package skeleton"
7
+ status: pending
8
+ - id: models
9
+ content: "Implement models.py: Newsletter, Post, Subscriber NamedTuples"
10
+ status: pending
11
+ - id: protocols
12
+ content: Define NewsletterRepository and EmailSender Protocol classes in db/__init__.py and email/__init__.py
13
+ status: pending
14
+ - id: db-sqlalchemy
15
+ content: Implement SQLAlchemyRepository (SQLite + Postgres, schema via create_all)
16
+ status: pending
17
+ - id: db-supabase-rest
18
+ content: Implement SupabaseRestRepository using HTTP REST API
19
+ status: pending
20
+ - id: email-mailgun
21
+ content: Implement MailgunSender (send_confirmation, send_post with batch sending)
22
+ status: pending
23
+ - id: email-smtp
24
+ content: Implement SmtpSender using stdlib smtplib
25
+ status: pending
26
+ - id: auth
27
+ content: Implement HTTP Basic Auth decorator in auth.py
28
+ status: pending
29
+ - id: routes
30
+ content: Implement all routes in routes.py, mode-aware (web vs api)
31
+ status: pending
32
+ - id: templates
33
+ content: Create minimal default templates under templates/quillet/ (public + admin UI)
34
+ status: pending
35
+ - id: admin-ui
36
+ content: Implement admin UI routes in admin.py (post list, new/edit post form, subscriber list), protected by Basic Auth, disabled via config
37
+ status: pending
38
+ - id: cli
39
+ content: Implement Flask CLI commands in cli.py
40
+ status: pending
41
+ - id: create-app
42
+ content: Implement create_blueprint() and create_app() in __init__.py, env-var driven for standalone
43
+ status: pending
44
+ isProject: false
45
+ ---
46
+
47
+ # QUILLET
48
+
49
+ Flask newsletter + microblog package. Importable Blueprint, standalone web app, or headless API.
50
+
51
+ ---
52
+
53
+ ## Repo structure (new standalone repo)
54
+
55
+ ```
56
+ quillet/
57
+ ├── quillet/
58
+ │ ├── __init__.py # public API: create_blueprint, create_app
59
+ │ ├── models.py # Newsletter, Post, Subscriber NamedTuples
60
+ │ ├── auth.py # HTTP Basic Auth decorator
61
+ │ ├── routes.py # public route handlers (mode-aware)
62
+ │ ├── admin.py # admin UI route handlers (Basic Auth, optional)
63
+ │ ├── cli.py # flask quillet ... CLI commands
64
+ │ ├── db/
65
+ │ │ ├── __init__.py # NewsletterRepository Protocol
66
+ │ │ ├── sqlalchemy.py # SQLAlchemy impl (SQLite + Postgres/Supabase)
67
+ │ │ └── supabase_rest.py # Supabase REST API impl (no connection string)
68
+ │ ├── email/
69
+ │ │ ├── __init__.py # EmailSender Protocol
70
+ │ │ ├── mailgun.py # Mailgun HTTP API
71
+ │ │ └── smtp.py # generic SMTP (stdlib smtplib)
72
+ │ └── templates/
73
+ │ └── quillet/ # namespaced to avoid host-app collisions
74
+ │ ├── base.html
75
+ │ ├── post_list.html
76
+ │ ├── post_detail.html
77
+ │ ├── subscribe_confirm.html
78
+ │ └── admin/
79
+ │ ├── dashboard.html # post list + subscriber count
80
+ │ ├── post_form.html # create / edit post
81
+ │ └── subscribers.html
82
+ ├── Dockerfile
83
+ ├── docker-compose.yml # with SQLite volume mount example
84
+ ├── pyproject.toml
85
+ └── README.md
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Three modes
91
+
92
+ ```mermaid
93
+ flowchart TD
94
+ user["User / Client"]
95
+ user -->|"import + register_blueprint"| modeA
96
+ user -->|"Docker + QUILLET_MODE=web"| modeB
97
+ user -->|"Docker + QUILLET_MODE=api"| modeC
98
+
99
+ subgraph modeA [Mode 1: Blueprint]
100
+ bp["create_blueprint(db=..., email=..., password=...)"]
101
+ bp --> hostapp["Host Flask app\n(tinthe.dev, etc.)"]
102
+ end
103
+
104
+ subgraph modeB [Mode 2: Standalone Web]
105
+ webApp["create_app() reads env vars\nHTML + JSON routes"]
106
+ end
107
+
108
+ subgraph modeC [Mode 3: Headless API]
109
+ apiApp["create_app() reads env vars\nJSON routes only"]
110
+ end
111
+ ```
112
+
113
+
114
+
115
+ ### Mode 1 — Blueprint (1-5 lines of integration)
116
+
117
+ ```python
118
+ from quillet import create_blueprint
119
+ from quillet.db.sqlalchemy import SQLAlchemyRepository
120
+ from quillet.email.mailgun import MailgunSender
121
+
122
+ app.register_blueprint(
123
+ create_blueprint(
124
+ db=SQLAlchemyRepository("sqlite:///newsletter.db"),
125
+ email=MailgunSender(api_key="...", domain="..."),
126
+ admin_password="secret",
127
+ ),
128
+ url_prefix="/newsletter",
129
+ )
130
+ ```
131
+
132
+ ### Mode 2 / 3 — Docker (env-var driven)
133
+
134
+ `create_app()` reads all config from env vars:
135
+
136
+ - `QUILLET_MODE` = `web` (default) | `api`
137
+ - `QUILLET_ADMIN_PASSWORD`
138
+ - `QUILLET_DB_BACKEND` = `sqlalchemy` | `supabase_rest`
139
+ - `QUILLET_DB_URL` (SQLAlchemy connection string)
140
+ - `QUILLET_SUPABASE_URL`, `QUILLET_SUPABASE_KEY`
141
+ - `QUILLET_EMAIL_BACKEND` = `mailgun` | `smtp`
142
+ - `QUILLET_MAILGUN_API_KEY`, `QUILLET_MAILGUN_DOMAIN`
143
+ - `QUILLET_SMTP_HOST`, `QUILLET_SMTP_PORT`, etc.
144
+
145
+ ---
146
+
147
+ ## Template override mechanism (Mode 2)
148
+
149
+ Flask resolves templates: **app templates dir > blueprint templates dir**.
150
+
151
+ - **Blueprint mode**: host app places overrides at `templates/quillet/post_list.html` — Flask finds them first automatically. No config needed.
152
+ - **Standalone Docker**: mount a volume to `/app/templates/quillet/`. The Dockerfile exposes this path. Default templates are minimal/unstyled so they're easy to replace.
153
+
154
+ ---
155
+
156
+ ## Data models
157
+
158
+ ```python
159
+ # models.py — all NamedTuples, immutable
160
+ class Newsletter(NamedTuple):
161
+ id: int
162
+ slug: str
163
+ name: str
164
+ from_email: str
165
+ from_name: str
166
+ reply_to: str | None
167
+
168
+ class Post(NamedTuple):
169
+ id: int
170
+ newsletter_id: int
171
+ slug: str
172
+ title: str
173
+ body_md: str
174
+ published_at: datetime | None
175
+ sent_at: datetime | None # None = not yet emailed
176
+
177
+ class Subscriber(NamedTuple):
178
+ id: int
179
+ newsletter_id: int
180
+ email: str
181
+ token: str # used for confirm + unsubscribe URLs
182
+ confirmed_at: datetime | None
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Plugin protocols
188
+
189
+ ```python
190
+ # db/__init__.py
191
+ class NewsletterRepository(Protocol):
192
+ def get_newsletter(self, slug: str) -> Newsletter | None: ...
193
+ def list_posts(self, newsletter_slug: str, published_only: bool = True) -> list[Post]: ...
194
+ def get_post(self, newsletter_slug: str, post_slug: str) -> Post | None: ...
195
+ def create_post(self, newsletter_slug: str, title: str, body_md: str) -> Post: ...
196
+ def publish_post(self, post_id: int) -> Post: ...
197
+ def mark_sent(self, post_id: int) -> None: ...
198
+ def add_subscriber(self, newsletter_slug: str, email: str) -> Subscriber: ...
199
+ def confirm_subscriber(self, token: str) -> Subscriber | None: ...
200
+ def list_confirmed_subscribers(self, newsletter_slug: str) -> list[Subscriber]: ...
201
+ def unsubscribe(self, token: str) -> None: ...
202
+
203
+ # email/__init__.py
204
+ class EmailSender(Protocol):
205
+ def send_confirmation(self, newsletter: Newsletter, subscriber: Subscriber, confirm_url: str) -> None: ...
206
+ def send_post(self, newsletter: Newsletter, post: Post, subscribers: list[Subscriber]) -> None: ...
207
+ ```
208
+
209
+ Anyone can implement these protocols to add new backends (Postmark, DynamoDB, etc.) without touching library internals.
210
+
211
+ ---
212
+
213
+ ## Routes (multi-newsletter, all prefixed by `/<newsletter_slug>`)
214
+
215
+
216
+ | Method | Path | Auth | Notes |
217
+ | ------ | ----------------------------------------- | ---------- | --------------------------------------------------- |
218
+ | GET | `/<slug>/` | — | post list |
219
+ | GET | `/<slug>/posts/<post_slug>` | — | post detail |
220
+ | POST | `/<slug>/subscribe` | — | creates unconfirmed subscriber, sends confirm email |
221
+ | GET | `/<slug>/confirm/<token>` | — | marks subscriber confirmed |
222
+ | GET | `/<slug>/unsubscribe/<token>` | — | removes subscriber |
223
+ | POST | `/<slug>/admin/posts` | Basic Auth | create post |
224
+ | POST | `/<slug>/admin/posts/<post_slug>/publish` | Basic Auth | set published_at |
225
+ | POST | `/<slug>/admin/posts/<post_slug>/send` | Basic Auth | email all confirmed subscribers |
226
+ | GET | `/<slug>/admin/subscribers` | Basic Auth | list subscribers |
227
+
228
+
229
+ In `api` mode: all responses are JSON. In `web` mode: HTML for public routes, JSON for admin routes (admin is always API-shaped).
230
+
231
+ ---
232
+
233
+ ## CLI
234
+
235
+ ```bash
236
+ flask quillet create "My Blog" --slug=blog
237
+ flask quillet list-newsletters
238
+ flask quillet send blog <post-slug> # manual send trigger
239
+ flask quillet subscribers blog
240
+ ```
241
+
242
+ ---
243
+
244
+ ## DB: SQLAlchemy vs Supabase REST
245
+
246
+ - `SQLAlchemyRepository` — uses SQLAlchemy 2.x Core (not ORM), handles SQLite and Postgres transparently. Runs `create_all()` on first use. This covers 95% of use cases including Supabase via its Postgres connection string.
247
+ - `SupabaseRestRepository` — uses Supabase's HTTP REST API (like the current tinthe `/book-call` pattern). For serverless / API-key-only deployments where a persistent DB connection isn't available.
248
+
249
+ ---
250
+
251
+ ## pyproject.toml optional extras
252
+
253
+ ```toml
254
+ [project.optional-dependencies]
255
+ sqlalchemy = ["SQLAlchemy>=2.0"]
256
+ mailgun = ["requests>=2.31"]
257
+ smtp = [] # stdlib only
258
+ supabase = ["requests>=2.31"]
259
+ all = ["SQLAlchemy>=2.0", "requests>=2.31"]
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Admin UI
265
+
266
+ A minimal, dependency-free HTML interface living at `/<slug>/admin/`. All routes require Basic Auth. Enabled by default, disabled via:
267
+
268
+ - Blueprint: `create_blueprint(..., admin_ui=False)`
269
+ - Standalone/Docker: `QUILLET_ADMIN_UI=false`
270
+
271
+ In `api` mode the admin UI is automatically disabled (API-only, no HTML).
272
+
273
+ ### Admin routes
274
+
275
+ - `GET /<slug>/admin/` — dashboard: paginated post list with status badges (draft / published / sent), total subscriber count
276
+ - `GET /<slug>/admin/posts/new` — blank post form
277
+ - `POST /<slug>/admin/posts/new` — create post, redirect to edit view
278
+ - `GET /<slug>/admin/posts/<post_slug>/edit` — edit post (title, body, slug)
279
+ - `POST /<slug>/admin/posts/<post_slug>/edit` — save edits
280
+ - `POST /<slug>/admin/posts/<post_slug>/publish` — set `published_at`, redirect to dashboard
281
+ - `POST /<slug>/admin/posts/<post_slug>/send` — trigger email send, redirect to dashboard
282
+ - `GET /<slug>/admin/subscribers` — subscriber list with confirm status
283
+
284
+ ### UI philosophy
285
+
286
+ - Plain HTML forms, zero JS, zero external CSS dependencies
287
+ - Inline `<style>` block, ~50 lines — functional, not pretty
288
+ - All actions are POST forms (no AJAX) so it works without JS and is trivially overridable
289
+ - Template override works the same way: drop `templates/quillet/admin/dashboard.html` in your host app to replace the default
290
+
291
+ ---
292
+
293
+ ## What this is NOT
294
+
295
+ - No admin UI (CRUD is via API/CLI — keeps scope tight, though an optional built-in UI is available)
296
+ - No comment system (reply-to-email approach; Giscus can be added in templates)
297
+ - No auth beyond Basic Auth (host app can wrap routes with its own decorator)
298
+
@@ -0,0 +1,41 @@
1
+ ---
2
+ description:
3
+ globs:
4
+ alwaysApply: true
5
+ ---
6
+ Write code that is robust.
7
+ Write code that is pragmatic.
8
+ Do not overexplain.
9
+ Focus on performance and simplicity, use high level abstractions.
10
+ Always try to find a ready-made solution.
11
+ Always search for existing implementations and use existing project patterns.
12
+ Make sure you never reimplement an existing pattern without approval.
13
+ We are writing well behaved Python code with latest standards.
14
+ We apply typing to function arguments and returns.
15
+ We prefer to invert conditionals and exit, when possible, example:
16
+ ```
17
+ # Bad:
18
+ if user:
19
+ # logic
20
+ else:
21
+ raise Exception("No user")
22
+
23
+ # Good:
24
+ if not user:
25
+ raise Exception("No user")
26
+ # logic
27
+ ```
28
+ We prefer to de-indent code where it is possible.
29
+ We prefer to use list comprehension.
30
+ We prefer to define NamedTuple instead of using dictionaries for data structures.
31
+ We prefer immutable structures (NamedTuple, frozen dataclasses, etc.) where mutation is unnecessary.
32
+ We prefer to keep functions short, up to 15 semantic lines.
33
+ We use generous whitespace and blank lines to group code logically and enhance readability.
34
+ We prefer to be explicit and verbose instead of using short variable names.
35
+ We prefer specific exceptions and avoid catching broad Exception unless justified.
36
+ We only import at the top of the module, outside functions. If importing inside a function is absolutely necessary (e.g., for type-checking or avoiding circular imports), the exception must be documented clearly.
37
+ We use relative imports for our code.
38
+ We import smallest possible units: `from foo import bar` is good. `import foo; foo.bar()` is to be avoided.
39
+ We avoid defining classes ONLY TO encapsulate methods, we rely on Python's modules.
40
+ We avoid commenting self-explanatory code, our comments are focused to docstrings (in functions) and to large scope explanations.
41
+ We avoid deep nesting (e.g., >2 levels). Break into smaller functions when needed.
@@ -0,0 +1,12 @@
1
+ ---
2
+ alwaysApply: true
3
+ ---
4
+ # Python Edit Policy
5
+
6
+ After every edit to a `.py` file, run `read_lints` on the `tinthe/` directory and fix any
7
+ errors before considering the task done. Never hand back to the user with outstanding lint errors.
8
+
9
+ Common mistakes to watch for in this codebase:
10
+ - Stale imports after feature removal (e.g. removing a route but leaving its `import`)
11
+ - Passing a `Path` object to a function that calls string methods (`.split`, `[0]`, etc.) — always check the type the callee expects
12
+ - Bare relative imports like `from foo import ...` instead of `from .foo import ...` inside the `tinthe` package
@@ -0,0 +1,74 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: pip install -e ".[all,dev]"
17
+ - run: ruff check .
18
+ - run: black --check . -l 120
19
+ - run: python scripts/smoke_test.py
20
+
21
+ pypi:
22
+ needs: test
23
+ runs-on: ubuntu-latest
24
+ environment: pypi
25
+ permissions:
26
+ id-token: write # required for OIDC trusted publishing
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: "3.12"
33
+
34
+ - name: Build
35
+ run: |
36
+ pip install build
37
+ python -m build
38
+
39
+ - name: Publish to PyPI
40
+ uses: pypa/gh-action-pypi-publish@release/v1
41
+
42
+ docker:
43
+ needs: test
44
+ runs-on: ubuntu-latest
45
+ permissions:
46
+ contents: read
47
+ packages: write # required to push to GHCR
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+
51
+ - name: Log in to GHCR
52
+ uses: docker/login-action@v3
53
+ with:
54
+ registry: ghcr.io
55
+ username: ${{ github.actor }}
56
+ password: ${{ secrets.GITHUB_TOKEN }}
57
+
58
+ - name: Extract version from tag
59
+ id: meta
60
+ uses: docker/metadata-action@v5
61
+ with:
62
+ images: ghcr.io/${{ github.repository }}
63
+ tags: |
64
+ type=semver,pattern={{version}}
65
+ type=semver,pattern={{major}}.{{minor}}
66
+ type=raw,value=latest
67
+
68
+ - name: Build and push
69
+ uses: docker/build-push-action@v6
70
+ with:
71
+ context: .
72
+ push: true
73
+ tags: ${{ steps.meta.outputs.tags }}
74
+ labels: ${{ steps.meta.outputs.labels }}
@@ -0,0 +1,28 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+
18
+ - name: Install
19
+ run: pip install -e ".[all,dev]"
20
+
21
+ - name: Lint (ruff)
22
+ run: ruff check .
23
+
24
+ - name: Format check (black)
25
+ run: black --check . -l 120
26
+
27
+ - name: Smoke test
28
+ run: python scripts/smoke_test.py
@@ -0,0 +1,14 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .eggs/
9
+ *.db
10
+ *.db-shm
11
+ *.db-wal
12
+ .env
13
+ .env.*
14
+ !.env.example
@@ -0,0 +1,22 @@
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY pyproject.toml README.md ./
6
+ RUN pip install --no-cache-dir ".[all]" gunicorn
7
+
8
+ COPY quillet/ quillet/
9
+ COPY scripts/ scripts/
10
+
11
+ # Mountable custom templates directory — takes precedence over built-in templates.
12
+ # docker run -v ./my-templates:/app/templates/quillet ...
13
+ VOLUME ["/app/templates"]
14
+
15
+ ENV QUILLET_MODE=web
16
+ ENV QUILLET_DB_BACKEND=sqlalchemy
17
+ ENV QUILLET_DB_URL=sqlite:////data/quillet.db
18
+ ENV QUILLET_EMAIL_BACKEND=smtp
19
+
20
+ EXPOSE 8000
21
+
22
+ CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "quillet._app:application"]
quillet-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tin Marković
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.