simple-module-dashboard 0.0.1__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.
dashboard/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Dashboard module."""
File without changes
@@ -0,0 +1,17 @@
1
+ """REST API endpoints for the Dashboard module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter, Depends, Request
6
+ from simple_module_db.deps import get_db
7
+ from sqlalchemy.ext.asyncio import AsyncSession
8
+
9
+ from dashboard.stats import fetch_dashboard_stats
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ @router.get("/stats")
15
+ async def dashboard_stats(request: Request, db: AsyncSession = Depends(get_db)) -> dict:
16
+ """Return dashboard statistics including user counts and system info."""
17
+ return await fetch_dashboard_stats(db, request.app)
@@ -0,0 +1,36 @@
1
+ """Inertia view endpoints for the Dashboard.
2
+
3
+ Mounted under ``/dashboard`` via :attr:`DashboardModule.meta.view_prefix`.
4
+ The public landing page at ``/`` is owned by the host, not this module.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from fastapi import APIRouter, Depends, Request
10
+ from inertia import InertiaResponse
11
+ from simple_module_db.deps import get_db
12
+ from simple_module_hosting.i18n_deps import TranslatorDep
13
+ from simple_module_hosting.inertia_deps import InertiaDep
14
+ from sqlalchemy.ext.asyncio import AsyncSession
15
+
16
+ from dashboard.stats import fetch_dashboard_stats
17
+
18
+ router = APIRouter()
19
+
20
+
21
+ @router.get("/", response_model=None)
22
+ async def dashboard(
23
+ request: Request,
24
+ inertia: InertiaDep,
25
+ t: TranslatorDep,
26
+ db: AsyncSession = Depends(get_db),
27
+ ) -> InertiaResponse:
28
+ """Authenticated dashboard — requires login (enforced by AuthMiddleware)."""
29
+ stats = await fetch_dashboard_stats(db, request.app)
30
+ return await inertia.render(
31
+ "Dashboard/Home",
32
+ {
33
+ "welcome": t.t("dashboard.home.welcome_message"),
34
+ **stats,
35
+ },
36
+ )
@@ -0,0 +1,21 @@
1
+ {
2
+ "home": {
3
+ "title": "Dashboard",
4
+ "description": "Overview of your application",
5
+ "stats": {
6
+ "total_users": "Total Users",
7
+ "active_users": "Active Users (7d)",
8
+ "products": "Products",
9
+ "modules": "Modules"
10
+ },
11
+ "system_info_title": "System",
12
+ "system_info": {
13
+ "modules": "Modules",
14
+ "python_version": "Python Version",
15
+ "health_checks": "Health Checks"
16
+ },
17
+ "welcome_card_title": "Welcome",
18
+ "welcome_message": "Welcome to SimpleModule",
19
+ "description_body": "This is a modular monolith built with FastAPI, Inertia.js, and React. Each module provides its own pages, API endpoints, and database schema."
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "home": {
3
+ "title": "Panel",
4
+ "description": "Resumen de tu aplicación",
5
+ "stats": {
6
+ "total_users": "Usuarios Totales",
7
+ "active_users": "Usuarios Activos (7d)",
8
+ "products": "Productos",
9
+ "modules": "Módulos"
10
+ },
11
+ "system_info_title": "Sistema",
12
+ "system_info": {
13
+ "modules": "Módulos",
14
+ "python_version": "Versión de Python",
15
+ "health_checks": "Verificaciones de Salud"
16
+ },
17
+ "welcome_card_title": "Bienvenido",
18
+ "welcome_message": "Bienvenido a SimpleModule",
19
+ "description_body": "Este es un monolito modular construido con FastAPI, Inertia.js y React. Cada módulo proporciona sus propias páginas, endpoints de API y esquema de base de datos."
20
+ }
21
+ }
dashboard/module.py ADDED
@@ -0,0 +1,45 @@
1
+ """Dashboard module definition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.resources
6
+ from pathlib import Path
7
+
8
+ from fastapi import APIRouter
9
+ from simple_module_core.menu import MenuItem, MenuRegistry, MenuSection
10
+ from simple_module_core.module import ModuleBase, ModuleMeta
11
+
12
+ _MODULE_PRODUCTS = "Products"
13
+ _MODULE_USERS = "Users"
14
+ _URL_DASHBOARD = "/dashboard/"
15
+ _ICON_DASHBOARD = "home"
16
+
17
+
18
+ class DashboardModule(ModuleBase):
19
+ meta = ModuleMeta(
20
+ name="Dashboard",
21
+ route_prefix="/api/dashboard",
22
+ view_prefix="/dashboard",
23
+ depends_on=[_MODULE_PRODUCTS, _MODULE_USERS],
24
+ )
25
+
26
+ def register_routes(self, api_router: APIRouter, view_router: APIRouter) -> None:
27
+ from dashboard.endpoints.api import router as api
28
+ from dashboard.endpoints.views import router as views
29
+
30
+ api_router.include_router(api)
31
+ view_router.include_router(views)
32
+
33
+ def register_menu_items(self, registry: MenuRegistry) -> None:
34
+ registry.add(
35
+ MenuItem(
36
+ label="Dashboard",
37
+ url=_URL_DASHBOARD,
38
+ icon=_ICON_DASHBOARD,
39
+ order=1,
40
+ section=MenuSection.SIDEBAR,
41
+ )
42
+ )
43
+
44
+ def locale_dirs(self) -> dict[str, Path]:
45
+ return {"dashboard": Path(str(importlib.resources.files(__package__) / "locales"))}
dashboard/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@simple-module-py/dashboard",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Frontend assets for the Dashboard module",
6
+ "peerDependencies": {
7
+ "react": "^19.0.0",
8
+ "react-dom": "^19.0.0",
9
+ "@inertiajs/react": "^2.0.0",
10
+ "@simple-module-py/ui": "*"
11
+ },
12
+ "devDependencies": {
13
+ "@simple-module-py/tsconfig": "*"
14
+ },
15
+ "dependencies": {}
16
+ }
@@ -0,0 +1,186 @@
1
+ import { usePage } from '@inertiajs/react';
2
+ import { keys, useT } from '@simple-module-py/i18n';
3
+ import { PageShell } from '@simple-module-py/ui/components/PageShell';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@simple-module-py/ui/components/ui/card';
5
+ import { Table, TableBody, TableCell, TableRow } from '@simple-module-py/ui/components/ui/table';
6
+ import { AuthenticatedLayout } from '@simple-module-py/ui/layouts/AuthenticatedLayout';
7
+ import { Activity, Box, Heart, Package, Server, Users } from 'lucide-react';
8
+
9
+ type Accent = 'primary' | 'emerald' | 'violet' | 'amber';
10
+
11
+ const HEALTH_STATUS_COLOR: Record<string, string> = {
12
+ healthy: 'bg-emerald-500',
13
+ degraded: 'bg-amber-500',
14
+ unhealthy: 'bg-red-500',
15
+ };
16
+
17
+ interface SystemModule {
18
+ name: string;
19
+ status: 'loaded';
20
+ }
21
+
22
+ interface HealthCheck {
23
+ name: string;
24
+ status: 'healthy' | 'degraded' | 'unhealthy';
25
+ }
26
+
27
+ interface SystemInfo {
28
+ modules: SystemModule[];
29
+ python_version: string;
30
+ health_checks: HealthCheck[];
31
+ }
32
+
33
+ interface Props {
34
+ total_users: number;
35
+ active_users_7d: number;
36
+ total_products: number;
37
+ module_count: number;
38
+ system_info: SystemInfo;
39
+ }
40
+
41
+ function Home() {
42
+ const props = usePage<{ props: Props }>().props as unknown as Props;
43
+ const { t } = useT();
44
+
45
+ return (
46
+ <PageShell
47
+ title={t(keys.dashboard.home.title)}
48
+ description={t(keys.dashboard.home.description)}
49
+ >
50
+ <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-5 mb-8">
51
+ <StatCard
52
+ title={t(keys.dashboard.home.stats.total_users)}
53
+ value={String(props.total_users)}
54
+ icon={<Users className="size-4" />}
55
+ accent="emerald"
56
+ />
57
+ <StatCard
58
+ title={t(keys.dashboard.home.stats.active_users)}
59
+ value={String(props.active_users_7d)}
60
+ icon={<Activity className="size-4" />}
61
+ accent="amber"
62
+ />
63
+ <StatCard
64
+ title={t(keys.dashboard.home.stats.products)}
65
+ value={String(props.total_products)}
66
+ icon={<Package className="size-4" />}
67
+ accent="primary"
68
+ />
69
+ <StatCard
70
+ title={t(keys.dashboard.home.stats.modules)}
71
+ value={String(props.module_count)}
72
+ icon={<Box className="size-4" />}
73
+ accent="violet"
74
+ />
75
+ </div>
76
+
77
+ <Card>
78
+ <CardHeader>
79
+ <CardTitle className="flex items-center gap-2 font-[var(--font-display)]">
80
+ <Server className="size-4" />
81
+ {t(keys.dashboard.home.system_info_title)}
82
+ </CardTitle>
83
+ </CardHeader>
84
+ <CardContent className="space-y-6">
85
+ <div>
86
+ <h4 className="text-sm font-medium text-muted-foreground mb-2">
87
+ {t(keys.dashboard.home.system_info.modules)}
88
+ </h4>
89
+ <div className="flex flex-wrap gap-2">
90
+ {props.system_info.modules.map((mod) => (
91
+ <span
92
+ key={mod.name}
93
+ className="inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium"
94
+ >
95
+ <span className="size-1.5 rounded-full bg-emerald-500" />
96
+ {mod.name}
97
+ </span>
98
+ ))}
99
+ </div>
100
+ </div>
101
+
102
+ <Table>
103
+ <TableBody>
104
+ <TableRow>
105
+ <TableCell className="font-medium text-muted-foreground">
106
+ {t(keys.dashboard.home.system_info.python_version)}
107
+ </TableCell>
108
+ <TableCell>{props.system_info.python_version}</TableCell>
109
+ </TableRow>
110
+ {props.system_info.health_checks.map((check) => (
111
+ <TableRow key={check.name}>
112
+ <TableCell className="font-medium text-muted-foreground flex items-center gap-2">
113
+ <Heart className="size-3" />
114
+ {check.name}
115
+ </TableCell>
116
+ <TableCell>
117
+ <span className="inline-flex items-center gap-1.5">
118
+ <span
119
+ className={`size-2 rounded-full ${HEALTH_STATUS_COLOR[check.status] ?? 'bg-red-500'}`}
120
+ />
121
+ {check.status}
122
+ </span>
123
+ </TableCell>
124
+ </TableRow>
125
+ ))}
126
+ </TableBody>
127
+ </Table>
128
+ </CardContent>
129
+ </Card>
130
+ </PageShell>
131
+ );
132
+ }
133
+
134
+ function StatCard({
135
+ title,
136
+ value,
137
+ icon,
138
+ accent,
139
+ }: {
140
+ title: string;
141
+ value: string;
142
+ icon: React.ReactNode;
143
+ accent: Accent;
144
+ }) {
145
+ const styles: Record<Accent, { card: string; icon: string; value: string }> = {
146
+ primary: {
147
+ card: 'border-primary-200 bg-gradient-to-br from-primary-50 to-card',
148
+ icon: 'text-primary-500 bg-primary-100',
149
+ value: 'text-primary-900',
150
+ },
151
+ emerald: {
152
+ card: 'border-emerald-border bg-gradient-to-br from-emerald-bg to-card',
153
+ icon: 'text-emerald-icon-fg bg-emerald-icon-bg',
154
+ value: 'text-emerald-value',
155
+ },
156
+ violet: {
157
+ card: 'border-violet-border bg-gradient-to-br from-violet-bg to-card',
158
+ icon: 'text-violet-icon-fg bg-violet-icon-bg',
159
+ value: 'text-violet-value',
160
+ },
161
+ amber: {
162
+ card: 'border-amber-200 bg-gradient-to-br from-amber-50 to-card',
163
+ icon: 'text-amber-600 bg-amber-100',
164
+ value: 'text-amber-900',
165
+ },
166
+ };
167
+
168
+ const s = styles[accent];
169
+
170
+ return (
171
+ <Card className={s.card}>
172
+ <CardContent className="pt-5">
173
+ <div className="flex items-center justify-between mb-3">
174
+ <span className="text-sm font-medium text-muted-foreground">{title}</span>
175
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${s.icon}`}>
176
+ {icon}
177
+ </div>
178
+ </div>
179
+ <p className={`text-3xl font-bold font-[var(--font-display)] ${s.value}`}>{value}</p>
180
+ </CardContent>
181
+ </Card>
182
+ );
183
+ }
184
+
185
+ Home.layout = (page: React.ReactNode) => <AuthenticatedLayout>{page}</AuthenticatedLayout>;
186
+ export default Home;
dashboard/py.typed ADDED
File without changes
dashboard/stats.py ADDED
@@ -0,0 +1,114 @@
1
+ """Dashboard statistics queries with TTL-based caching."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sys
7
+ import time
8
+ from datetime import UTC, datetime, timedelta
9
+
10
+ from fastapi import FastAPI
11
+ from products.models import Product
12
+ from simple_module_core.health import HealthCheck, HealthStatus
13
+ from sqlalchemy import func, select
14
+ from sqlalchemy.ext.asyncio import AsyncSession
15
+ from users.models import User
16
+
17
+ _CACHE_TTL_SECONDS = 30
18
+ _cache: dict | None = None
19
+ _cache_ts: float = 0.0
20
+ _cache_lock = asyncio.Lock()
21
+
22
+
23
+ def _cache_hit() -> dict | None:
24
+ if _cache is not None and (time.monotonic() - _cache_ts) < _CACHE_TTL_SECONDS:
25
+ return _cache.copy()
26
+ return None
27
+
28
+
29
+ async def fetch_dashboard_stats(db: AsyncSession, app: FastAPI) -> dict:
30
+ """Gather all dashboard statistics, cached for 30 seconds."""
31
+ global _cache, _cache_ts
32
+
33
+ hit = _cache_hit()
34
+ if hit is not None:
35
+ return hit
36
+
37
+ async with _cache_lock:
38
+ # Re-check after acquiring lock — another coroutine may have refreshed.
39
+ hit = _cache_hit()
40
+ if hit is not None:
41
+ return hit
42
+
43
+ total_users = await _count_users(db)
44
+ active_users_7d = await _count_active_users(db, days=7)
45
+ total_products = await _count_products(db)
46
+ modules_list = _get_module_info(app)
47
+ health_checks = await _run_health_checks(app)
48
+
49
+ result = {
50
+ "total_users": total_users,
51
+ "active_users_7d": active_users_7d,
52
+ "total_products": total_products,
53
+ "module_count": len(modules_list),
54
+ "system_info": {
55
+ "modules": modules_list,
56
+ "python_version": (
57
+ f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
58
+ ),
59
+ "health_checks": health_checks,
60
+ },
61
+ }
62
+
63
+ _cache = result
64
+ _cache_ts = time.monotonic()
65
+ return result.copy()
66
+
67
+
68
+ def invalidate_stats_cache() -> None:
69
+ """Clear the stats cache — useful for testing or after data mutations."""
70
+ global _cache, _cache_ts
71
+ _cache = None
72
+ _cache_ts = 0.0
73
+
74
+
75
+ async def _count_users(db: AsyncSession) -> int:
76
+ result = await db.execute(select(func.count()).select_from(User))
77
+ return result.scalar_one()
78
+
79
+
80
+ async def _count_active_users(db: AsyncSession, *, days: int) -> int:
81
+ cutoff = datetime.now(UTC) - timedelta(days=days)
82
+ result = await db.execute(
83
+ select(func.count()).select_from(User).where(User.last_login_at >= cutoff)
84
+ )
85
+ return result.scalar_one()
86
+
87
+
88
+ async def _count_products(db: AsyncSession) -> int:
89
+ result = await db.execute(
90
+ select(func.count()).select_from(Product).where(Product.is_active.is_(True))
91
+ )
92
+ return result.scalar_one()
93
+
94
+
95
+ def _get_module_info(app: FastAPI) -> list[dict[str, str]]:
96
+ # Reads from the module list discovered once at startup, avoiding
97
+ # expensive entry-point rescans on every request.
98
+ return [{"name": m.meta.name, "status": "loaded"} for m in app.state.sm.modules]
99
+
100
+
101
+ async def _run_health_checks(app: FastAPI) -> list[dict[str, str]]:
102
+ registry = app.state.sm.health_registry
103
+ checks = registry.all_checks
104
+ if not checks:
105
+ return []
106
+
107
+ async def _run_one(check: HealthCheck) -> dict[str, str]:
108
+ try:
109
+ result = await check.check()
110
+ return {"name": check.name, "status": result.status.value}
111
+ except Exception:
112
+ return {"name": check.name, "status": HealthStatus.UNHEALTHY.value}
113
+
114
+ return list(await asyncio.gather(*[_run_one(c) for c in checks]))
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: simple_module_dashboard
3
+ Version: 0.0.1
4
+ Summary: Admin landing page and sidebar menu host for authenticated users of a simple_module app
5
+ Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
+ Project-URL: Repository, https://github.com/antosubash/simple_module_python
7
+ Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
8
+ Project-URL: Changelog, https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md
9
+ Author-email: Anto Subash <antosubash@live.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: admin,dashboard,inertia,simple-module
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: simple-module-core==0.0.1
25
+ Requires-Dist: simple-module-db==0.0.1
26
+ Requires-Dist: simple-module-hosting==0.0.1
27
+ Requires-Dist: simple-module-products==0.0.1
28
+ Requires-Dist: simple-module-users==0.0.1
29
+ Description-Content-Type: text/markdown
30
+
31
+ # simple_module_dashboard
32
+
33
+ Admin landing page + sidebar menu host for authenticated users of a [simple_module](https://github.com/antosubash/simple_module_python) app. Renders `/dashboard`, collects menu entries registered by every other installed module, and provides the primary Inertia layout.
34
+
35
+ Pre-wired into any app scaffolded with `simple-module new`.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install simple_module_dashboard
41
+ ```
42
+
43
+ ## What it provides
44
+
45
+ - `/dashboard` Inertia view, a single entry point for logged-in users.
46
+ - Global sidebar renderer — aggregates `register_menu_items()` calls from all modules into one tree.
47
+ - Breadcrumb + page-title provider used by downstream module pages.
48
+
49
+ ## Usage
50
+
51
+ Install the module in a host, and any other module can register a menu entry:
52
+
53
+ ```python
54
+ # modules/orders/orders/module.py
55
+ from simple_module_core import ModuleBase, ModuleMeta
56
+ from simple_module_core.menus import MenuItem
57
+
58
+
59
+ class OrdersModule(ModuleBase):
60
+ meta = ModuleMeta(name="orders")
61
+
62
+ def register_menu_items(self):
63
+ return [MenuItem(label="Orders", href="/orders", icon="shopping-bag")]
64
+ ```
65
+
66
+ The dashboard sidebar picks it up automatically.
67
+
68
+ ## Depends on
69
+
70
+ - `simple_module_core`, `simple_module_db`, `simple_module_hosting`
71
+ - `simple_module_users`, `simple_module_products` (demo content used by the default layout)
72
+
73
+ ## License
74
+
75
+ MIT — see [LICENSE](https://github.com/antosubash/simple_module_python/blob/main/LICENSE).
@@ -0,0 +1,16 @@
1
+ dashboard/__init__.py,sha256=5c12Gf0Ehk8pjWvVP5stItAp63wtIRZB7yMY7ibZD44,24
2
+ dashboard/module.py,sha256=KfS1mWeA8DW4CJMLrgZyX-cvlj0UZ5eIh-fuRJcWips,1361
3
+ dashboard/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ dashboard/stats.py,sha256=eu1ygZeLhCiU_J_y7gsIF5w9aCfI-KWGSb16j7Gv2k4,3607
5
+ dashboard/endpoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ dashboard/endpoints/api.py,sha256=WPbzlnrByyr4cJBo51nNPhJMCVeOk09cnKC9igAD78A,544
7
+ dashboard/endpoints/views.py,sha256=lXGJ5-LLE9FtMZG0oP7UMDiIK9ustbjM-as0xfGCfGc,1097
8
+ dashboard/locales/en.json,sha256=uKLPMP5wIT6SbaLhanHZxDL_Uui5u4vDkZx3clopU3g,682
9
+ dashboard/locales/es.json,sha256=FVnhG-mCKhZcWlahBxufV6Z1NgYHJG0QP3-19Uen5cw,729
10
+ dashboard/pages/Home.tsx,sha256=6VmmjukBooSt6XQvrXpaafTJMJd3IpnJ5ou5ATOyrHM,6034
11
+ dashboard/package.json,sha256=rQk8G963uVGfmSjghde11OPEBgQrLfb7GpdKvibY-nU,381
12
+ simple_module_dashboard-0.0.1.dist-info/METADATA,sha256=0IBgXU7qYPohMhYHhKDbKmq4SD7FRsrrGJrZcAi86oI,2817
13
+ simple_module_dashboard-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
14
+ simple_module_dashboard-0.0.1.dist-info/entry_points.txt,sha256=ZETTXF8ULqw2Xv-nwuSEab5dgtdCsa2h6tITh6HwAEg,61
15
+ simple_module_dashboard-0.0.1.dist-info/licenses/LICENSE,sha256=Yn66lhLklsF5p7pa85_ksQrJ79Q-FgOaUAHevLBjer4,1068
16
+ simple_module_dashboard-0.0.1.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,2 @@
1
+ [simple_module]
2
+ dashboard = dashboard.module:DashboardModule
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anto Subash
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.