mgf-sqlalchemy 0.1.0__py3-none-any.whl

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,47 @@
1
+ """``mgf.sqlalchemy`` — async SQLAlchemy helpers for mgf-common consumers.
2
+
3
+ Sibling of :mod:`mgf.common` under the ``mgf.*`` namespace. Houses the
4
+ async-engine factory + sessionmaker + tenant-RLS helper that previously
5
+ lived under ``mgf.common.db.*`` — extracted at mgf-common v0.30 /
6
+ mgf-sqlalchemy v0.1 per the federation split plan.
7
+
8
+ Every async-SQLAlchemy consumer needs the same three things:
9
+
10
+ - An :class:`AsyncEngine` factory with sane defaults (pool sizing,
11
+ ``pool_pre_ping``, ``pool_recycle``, ``echo`` from settings).
12
+ - An :func:`async_sessionmaker` factory wired to that engine.
13
+ - A per-request tenant-scoping context manager
14
+ (``SET LOCAL app.current_tenant = '<uuid>'`` for Postgres RLS).
15
+
16
+ This sibling is **framework-agnostic**: no FastAPI, no Starlette, no
17
+ Django imports. The FastAPI-Depends-shaped ``get_session`` and the
18
+ ``setup_db`` lifespan helper that previously lived under
19
+ ``mgf.common.db`` were moved to the :mod:`mgf.fastapi.db` submodule
20
+ in mgf-fastapi v0.2.0 (which depends on mgf-sqlalchemy via the
21
+ ``[sqlalchemy]`` extra). See ``mgf-sqlalchemy/docs/cutover/v0.1.0.md``
22
+ for the full migration map.
23
+
24
+ Install: ``pip install mgf-sqlalchemy`` (pulls
25
+ ``sqlalchemy[asyncio]>=2.0``). For tests against an in-memory SQLite
26
+ database: ``pip install 'mgf-sqlalchemy[test]'`` (adds ``aiosqlite``).
27
+
28
+ The integration is **adapter-shaped, not framework-shaped**. Nothing
29
+ here re-exports SQLAlchemy primitives or hides them. The consumer's
30
+ ORM is still vanilla SQLAlchemy 2; these helpers plug into the seams
31
+ (engine factory, sessionmaker).
32
+
33
+ Closed-box invariant: ``mgf-sqlalchemy`` depends on ``mgf-common`` +
34
+ ``sqlalchemy`` only. Consumers MUST import from ``mgf.sqlalchemy``,
35
+ never reach into ``mgf.sqlalchemy._engine`` / ``mgf.sqlalchemy._session``.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ from mgf.sqlalchemy._engine import create_engine
41
+ from mgf.sqlalchemy._session import create_sessionmaker, tenant_session
42
+
43
+ __all__ = [
44
+ "create_engine",
45
+ "create_sessionmaker",
46
+ "tenant_session",
47
+ ]
@@ -0,0 +1,84 @@
1
+ """Async SQLAlchemy engine factory.
2
+
3
+ Wraps :func:`sqlalchemy.ext.asyncio.create_async_engine` with
4
+ production-leaning defaults:
5
+
6
+ - ``pool_pre_ping=True`` — every checkout pings the connection.
7
+ Costs one round-trip per checkout but catches stale connections
8
+ after DB restart / firewall reset / etc.
9
+ - ``pool_recycle=3600`` — recycle connections older than an hour.
10
+ Avoids the MySQL "wait_timeout" surprise + Postgres connection
11
+ leaks.
12
+ - ``pool_size`` / ``max_overflow`` set to conservative defaults
13
+ that work for SQLite + scale to Postgres / MySQL.
14
+
15
+ Consumers override any default via kwargs.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any
21
+
22
+ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
23
+
24
+ # Production-leaning defaults. Override per-consumer as needed.
25
+ _DEFAULT_POOL_PRE_PING: bool = True
26
+ _DEFAULT_POOL_RECYCLE_SECONDS: int = 3600 # 1 hour
27
+ _DEFAULT_POOL_SIZE: int = 5
28
+ _DEFAULT_MAX_OVERFLOW: int = 10
29
+
30
+
31
+ def create_engine(
32
+ database_url: str,
33
+ *,
34
+ echo: bool = False,
35
+ pool_pre_ping: bool = _DEFAULT_POOL_PRE_PING,
36
+ pool_recycle: int = _DEFAULT_POOL_RECYCLE_SECONDS,
37
+ pool_size: int = _DEFAULT_POOL_SIZE,
38
+ max_overflow: int = _DEFAULT_MAX_OVERFLOW,
39
+ **extra: Any,
40
+ ) -> AsyncEngine:
41
+ """Build an async SQLAlchemy engine with mgf defaults.
42
+
43
+ ``database_url`` follows the SQLAlchemy URL grammar — must use
44
+ an async driver:
45
+
46
+ - ``postgresql+asyncpg://user:pass@host/db``
47
+ - ``mysql+aiomysql://...``
48
+ - ``sqlite+aiosqlite://`` (in-memory) or
49
+ ``sqlite+aiosqlite:///path/to/db.sqlite``
50
+
51
+ ``echo=True`` logs every statement (debug only — never in prod).
52
+
53
+ Pool settings are passed through to SQLAlchemy. SQLite ignores
54
+ ``pool_size`` / ``max_overflow`` (it uses ``StaticPool`` /
55
+ ``NullPool``) — the values are still accepted for shape
56
+ consistency but have no effect.
57
+
58
+ ``**extra`` is forwarded to :func:`create_async_engine` for
59
+ consumers needing rare options (``connect_args``, ``isolation_level``,
60
+ ``poolclass``, etc.).
61
+
62
+ Returns the engine. The engine is **not yet connected** — the
63
+ first connection attempt happens lazily on the first
64
+ ``await engine.connect()`` or ``async_sessionmaker()`` call.
65
+ """
66
+ kwargs: dict[str, Any] = {
67
+ "echo": echo,
68
+ "pool_pre_ping": pool_pre_ping,
69
+ "pool_recycle": pool_recycle,
70
+ }
71
+
72
+ # Pool sizing only applies to drivers that use a real pool.
73
+ # SQLite uses StaticPool / NullPool; passing pool_size to those
74
+ # raises. Detect the driver and skip pool kwargs accordingly.
75
+ is_sqlite = database_url.startswith(("sqlite://", "sqlite+"))
76
+ if not is_sqlite:
77
+ kwargs["pool_size"] = pool_size
78
+ kwargs["max_overflow"] = max_overflow
79
+
80
+ kwargs.update(extra)
81
+ return create_async_engine(database_url, **kwargs)
82
+
83
+
84
+ __all__ = ["create_engine"]
@@ -0,0 +1,105 @@
1
+ """Async sessionmaker + tenant-scoping helper.
2
+
3
+ Two helpers covering the request-handling lifecycle:
4
+
5
+ - :func:`create_sessionmaker` — wraps :class:`async_sessionmaker`
6
+ with mgf defaults (``expire_on_commit=False`` — SQLAlchemy 2's
7
+ recommended default for async; consumers can pass
8
+ ``expire_on_commit=True`` to opt into the legacy behavior).
9
+ - :func:`tenant_session` — context manager that runs
10
+ ``SET LOCAL app.current_tenant = '<uuid>'`` for Postgres RLS-based
11
+ multi-tenancy. The ``LOCAL`` qualifier scopes to the current
12
+ transaction; auto-resets on commit / rollback. Tenant id is
13
+ UUID-validated at the call site to prevent SQL injection.
14
+
15
+ The FastAPI-Depends-shaped ``get_session`` previously co-located here
16
+ was moved to :mod:`mgf.fastapi.db` in mgf-fastapi v0.2 — it depends
17
+ on Starlette/FastAPI's ``Request`` / ``app.state`` which would pull a
18
+ heavy framework dep into this otherwise-pure sibling.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from contextlib import asynccontextmanager
24
+ from typing import TYPE_CHECKING
25
+ from uuid import UUID
26
+
27
+ from sqlalchemy import text
28
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
29
+
30
+ if TYPE_CHECKING:
31
+ from collections.abc import AsyncIterator
32
+
33
+ from sqlalchemy.ext.asyncio import AsyncEngine
34
+
35
+
36
+ def create_sessionmaker(
37
+ engine: AsyncEngine,
38
+ *,
39
+ expire_on_commit: bool = False,
40
+ ) -> async_sessionmaker[AsyncSession]:
41
+ """Return an :class:`async_sessionmaker` bound to ``engine``.
42
+
43
+ Default ``expire_on_commit=False`` matches SQLAlchemy 2's
44
+ recommended default for async sessions (objects stay usable
45
+ after commit instead of being silently expired). Pass
46
+ ``expire_on_commit=True`` to revert to the legacy synchronous
47
+ semantics.
48
+ """
49
+ return async_sessionmaker(
50
+ engine,
51
+ class_=AsyncSession,
52
+ expire_on_commit=expire_on_commit,
53
+ )
54
+
55
+
56
+ @asynccontextmanager
57
+ async def tenant_session(
58
+ session: AsyncSession,
59
+ tenant_id: str | UUID,
60
+ ) -> AsyncIterator[AsyncSession]:
61
+ """Run ``SET LOCAL app.current_tenant = '<tenant_id>'`` for ``session``.
62
+
63
+ Postgres RLS-based multi-tenancy pattern. The ``LOCAL`` qualifier
64
+ scopes the setting to the current transaction; auto-resets on
65
+ commit / rollback so cross-tenant leakage between requests is
66
+ impossible at the SQL layer.
67
+
68
+ ``tenant_id`` is UUID-validated before interpolation. Postgres
69
+ ``SET LOCAL`` does NOT accept bind parameters, so we have to
70
+ interpolate the value into the SQL — which is safe iff the
71
+ input is shape-validated (UUIDs are alphanumeric + dashes,
72
+ no SQL-meaningful chars). A non-UUID input raises ``ValueError``.
73
+
74
+ Raises ``ValueError`` if ``tenant_id`` isn't a UUID-shaped
75
+ string; raises whatever SQLAlchemy raises if the database
76
+ rejects the SET (e.g., custom GUC not declared in
77
+ ``postgresql.conf``).
78
+
79
+ Use as::
80
+
81
+ async with sessionmaker() as session:
82
+ async with tenant_session(session, tenant_id) as scoped:
83
+ # All queries on `scoped` (same session, just
84
+ # tenant-scoped) get app.current_tenant.
85
+ rows = await scoped.execute(text("SELECT ..."))
86
+ """
87
+ if isinstance(tenant_id, UUID):
88
+ tenant_str = str(tenant_id)
89
+ else:
90
+ # Validate that the string is a real UUID; UUID() raises
91
+ # ValueError on malformed input. After validation, the
92
+ # canonical hex+dash form is safe to interpolate.
93
+ validated = UUID(tenant_id)
94
+ tenant_str = str(validated)
95
+
96
+ # SET LOCAL accepts only literal values; bind params don't work.
97
+ # Safe because tenant_str came from UUID() — alphanumeric+dashes.
98
+ await session.execute(text(f"SET LOCAL app.current_tenant = '{tenant_str}'"))
99
+ yield session
100
+
101
+
102
+ __all__ = [
103
+ "create_sessionmaker",
104
+ "tenant_session",
105
+ ]
File without changes
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: mgf-sqlalchemy
3
+ Version: 0.1.0
4
+ Summary: Async SQLAlchemy helpers for mgf-common consumers — typed engine factory, sessionmaker, Postgres RLS tenant-scoping. Sibling of mgf-common under the mgf.* namespace.
5
+ Project-URL: Homepage, https://codeberg.org/magogi-admin/mgf-sqlalchemy
6
+ Project-URL: Issues, https://codeberg.org/magogi-admin/mgf-sqlalchemy/issues
7
+ Project-URL: Changelog, https://codeberg.org/magogi-admin/mgf-sqlalchemy/src/branch/main/CHANGELOG.md
8
+ Author: Bassam Alsanie, mgf-sqlalchemy contributors
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: async,asyncpg,engine,multi-tenant,rls,sqlalchemy
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Database
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: mgf-common<0.31,>=0.30
27
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: aiosqlite>=0.20; extra == 'dev'
30
+ Requires-Dist: import-linter>=2.0; extra == 'dev'
31
+ Requires-Dist: mypy>=1.10; extra == 'dev'
32
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
33
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
34
+ Requires-Dist: pytest>=8.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.4; extra == 'dev'
36
+ Provides-Extra: test
37
+ Requires-Dist: aiosqlite>=0.20; extra == 'test'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # `mgf-sqlalchemy` — async SQLAlchemy helpers for mgf-common consumers
41
+
42
+ [![PyPI](https://img.shields.io/pypi/v/mgf-sqlalchemy)](https://pypi.org/project/mgf-sqlalchemy/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/mgf-sqlalchemy)](https://pypi.org/project/mgf-sqlalchemy/)
44
+
45
+ > **Sibling of [`mgf-common`](https://pypi.org/project/mgf-common/)
46
+ > under the `mgf.*` namespace.** Houses the async-SQLAlchemy helpers
47
+ > that previously lived under `mgf.common.db.*` — extracted at
48
+ > mgf-common v0.30 / mgf-sqlalchemy v0.1 per the
49
+ > [federation split plan](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/release/federation_roadmap.md).
50
+
51
+ ## What this provides
52
+
53
+ | Submodule | What |
54
+ |---|---|
55
+ | `mgf.sqlalchemy` | `create_engine` — async SQLAlchemy engine factory with production-leaning pool defaults (`pool_pre_ping`, `pool_recycle`, sane `pool_size`/`max_overflow`). SQLite-aware (skips pool kwargs that StaticPool/NullPool reject). `create_sessionmaker` — async sessionmaker factory with SQLAlchemy 2's recommended `expire_on_commit=False` default. `tenant_session` — context manager for Postgres RLS multi-tenancy (`SET LOCAL app.current_tenant = '<uuid>'`); UUID-validated to prevent SQL injection. |
56
+
57
+ **FastAPI helpers live elsewhere.** The `get_session`
58
+ FastAPI-Depends generator and the `setup_db` lifespan helper that
59
+ previously co-located with these in mgf-common moved to
60
+ [`mgf.fastapi.db`](https://pypi.org/project/mgf-fastapi/) in
61
+ mgf-fastapi v0.2.0 (which depends on mgf-sqlalchemy via its
62
+ `[sqlalchemy]` extra). This sibling stays framework-agnostic — no
63
+ Starlette / no FastAPI in the dependency graph.
64
+
65
+ ## Install
66
+
67
+ ```bash
68
+ pip install mgf-sqlalchemy
69
+ # Or with the test extra (aiosqlite for in-memory SQLite tests):
70
+ pip install 'mgf-sqlalchemy[test]'
71
+ ```
72
+
73
+ Pulls in `mgf-common` + `sqlalchemy[asyncio]` automatically. Production
74
+ consumers also need an async driver of their own (asyncpg / aiomysql /
75
+ asyncmy); we don't pin one — the consumer picks based on their
76
+ database.
77
+
78
+ ## Quick start
79
+
80
+ ```python
81
+ import asyncio
82
+ from mgf.sqlalchemy import create_engine, create_sessionmaker
83
+
84
+ async def main() -> None:
85
+ engine = create_engine(
86
+ "postgresql+asyncpg://user:pass@localhost/myapp",
87
+ echo=False,
88
+ )
89
+ sessionmaker = create_sessionmaker(engine)
90
+ try:
91
+ async with sessionmaker() as session:
92
+ from sqlalchemy import text
93
+ row = (await session.execute(text("SELECT 1"))).scalar_one()
94
+ print(row)
95
+ finally:
96
+ await engine.dispose()
97
+
98
+ asyncio.run(main())
99
+ ```
100
+
101
+ ## Postgres RLS multi-tenancy
102
+
103
+ ```python
104
+ from uuid import UUID
105
+ from mgf.sqlalchemy import tenant_session
106
+
107
+ tenant_id = UUID("550e8400-e29b-41d4-a716-446655440000")
108
+
109
+ async with sessionmaker() as session:
110
+ async with tenant_session(session, tenant_id) as scoped:
111
+ # Every query on `scoped` (same session, just tenant-scoped)
112
+ # gets `app.current_tenant` set in the current transaction.
113
+ # RLS policies in your schema can read from
114
+ # `current_setting('app.current_tenant')`.
115
+ rows = await scoped.execute(text("SELECT ..."))
116
+ ```
117
+
118
+ ## FastAPI integration
119
+
120
+ The FastAPI-shaped helpers (request-scoped session injection +
121
+ lifespan) live in mgf-fastapi:
122
+
123
+ ```python
124
+ # pyproject.toml
125
+ dependencies = [
126
+ "mgf-common>=0.30,<0.31",
127
+ "mgf-sqlalchemy>=0.1,<0.2",
128
+ "mgf-fastapi[sqlalchemy]>=0.2,<0.3",
129
+ ]
130
+ ```
131
+
132
+ ```python
133
+ from typing import Annotated
134
+ from contextlib import asynccontextmanager
135
+ from fastapi import FastAPI, Depends
136
+ from sqlalchemy.ext.asyncio import AsyncSession
137
+ from mgf.fastapi.db import get_session, setup_db
138
+
139
+ @asynccontextmanager
140
+ async def lifespan(app: FastAPI):
141
+ async with setup_db(app, database_url="postgresql+asyncpg://..."):
142
+ yield
143
+
144
+ app = FastAPI(lifespan=lifespan)
145
+
146
+ @app.get("/users")
147
+ async def list_users(
148
+ session: Annotated[AsyncSession, Depends(get_session)],
149
+ ) -> list[dict]:
150
+ ...
151
+ ```
152
+
153
+ ## Documentation
154
+
155
+ - [`docs/recipes/sqlalchemy.md`](docs/recipes/sqlalchemy.md) — full async-SQLAlchemy walkthrough.
156
+ - [`docs/cutover/v0.1.0.md`](docs/cutover/v0.1.0.md) — maiden voyage migration story (the v0.30 split + the get_session/setup_db relocation to mgf-fastapi).
157
+ - [`PUBLIC_API.md`](PUBLIC_API.md) — full public surface contract.
158
+ - [`CHANGELOG.md`](CHANGELOG.md) — release history.
159
+
160
+ For the federation-wide engineering standards (DESIGN_PRINCIPLES,
161
+ ERROR_HANDLING, SECURITY, etc.) see
162
+ [`mgf-common/docs/standards/`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/standards/).
163
+ This sibling inherits them by reference; the standards
164
+ source-of-truth lives in mgf-common.
165
+
166
+ ## Status
167
+
168
+ 🚧 **Experimental** — every public name is `experimental` per AP-09.
169
+ Promotion to `stable` happens release-by-release as consumer feedback
170
+ in [`mgf-common/FEEDBACK.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/FEEDBACK.md)
171
+ converges. The 0.x window applies. Pin tightly:
172
+ `mgf-sqlalchemy = ">=0.X.0,<0.Y"`.
173
+
174
+ ## Cross-references
175
+
176
+ - **Filing process for sharp edges**: open an entry on
177
+ [`mgf-common/FEEDBACK.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/FEEDBACK.md)
178
+ with `[mgf-sqlalchemy]` prefix, OR file directly on this repo's
179
+ Issues → maintainer mirrors into the canonical FEEDBACK.md.
180
+ - **Federation pattern**:
181
+ [`mgf-common/docs/design/federation.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/design/federation.md).
182
+ - **The split that created this sibling**:
183
+ [`mgf-common/docs/release/federation_roadmap.md`](https://codeberg.org/magogi-admin/mgf_common/src/branch/main/docs/release/federation_roadmap.md).
184
+ - **Companion sibling**: [`mgf-alembic`](https://pypi.org/project/mgf-alembic/) — async-aware alembic env.py helper (paired ship at v0.30; depends on this sibling).
@@ -0,0 +1,8 @@
1
+ mgf/sqlalchemy/__init__.py,sha256=BG51-RLTY_RUCKRTtYiU7-3rSYHKE8m9V7J_UxTdfhg,2030
2
+ mgf/sqlalchemy/_engine.py,sha256=2ofgYljzOxjoeVp5kmv0Bhq2WDUsDFgaG2QkOTiPq8c,2891
3
+ mgf/sqlalchemy/_session.py,sha256=IRT6y61ODm9BuA3yx-mPWAjJACMR1CLkqjPcGbNq8vE,3850
4
+ mgf/sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ mgf_sqlalchemy-0.1.0.dist-info/METADATA,sha256=Me6qLm1JBn14u6lxmXBpYoInbZlvHqul5wUTMneqP8M,7580
6
+ mgf_sqlalchemy-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ mgf_sqlalchemy-0.1.0.dist-info/licenses/LICENSE,sha256=akPl7tlbK9cB--mcmU-3T3VYoIpSXQUJd3DDpztyDFc,1099
8
+ mgf_sqlalchemy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bassam Alsanie and mgf-common 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.