dirigent-server 0.9.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.
- dirigent_server/__init__.py +7 -0
- dirigent_server/app.py +171 -0
- dirigent_server/dependencies.py +51 -0
- dirigent_server/errors.py +116 -0
- dirigent_server/health.py +97 -0
- dirigent_server/logging.py +5 -0
- dirigent_server/pagination.py +56 -0
- dirigent_server/py.typed +0 -0
- dirigent_server/routes/__init__.py +79 -0
- dirigent_server/routes/alerts.py +289 -0
- dirigent_server/routes/auth.py +218 -0
- dirigent_server/routes/blocks.py +39 -0
- dirigent_server/routes/connections.py +294 -0
- dirigent_server/routes/hooks.py +114 -0
- dirigent_server/routes/pipelines.py +427 -0
- dirigent_server/routes/runs.py +818 -0
- dirigent_server/routes/schema.py +27 -0
- dirigent_server/routes/schemas.py +127 -0
- dirigent_server/routes/system.py +63 -0
- dirigent_server/routes/trigger_documents.py +109 -0
- dirigent_server/routes/triggers.py +537 -0
- dirigent_server/routes/users.py +226 -0
- dirigent_server/routes/workers.py +52 -0
- dirigent_server/security.py +186 -0
- dirigent_server/static/.gitkeep +0 -0
- dirigent_server/transactions.py +34 -0
- dirigent_server/ui.py +237 -0
- dirigent_server-0.9.0.dist-info/METADATA +17 -0
- dirigent_server-0.9.0.dist-info/RECORD +32 -0
- dirigent_server-0.9.0.dist-info/WHEEL +4 -0
- dirigent_server-0.9.0.dist-info/licenses/LICENSE +18 -0
- dirigent_server-0.9.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +631 -0
dirigent_server/ui.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""The web UI: a built single-page bundle, served off the API server itself.
|
|
2
|
+
|
|
3
|
+
The bundle mounts in two pieces, and the order of the two matters. Starlette matches routes
|
|
4
|
+
in registration order, so ``/assets`` is registered with the fixed paths, ahead of every
|
|
5
|
+
router, and the shell is mounted at ``/`` after them, where it claims whatever is left.
|
|
6
|
+
|
|
7
|
+
The UI routes with clean paths, so ``/runs/<id>`` is a route inside the bundle rather than a
|
|
8
|
+
file on disk. Nothing serves it, and a deep link or a refresh would answer 404 -- so the 404
|
|
9
|
+
handler serves the shell for a navigation that no route and no file claimed. A navigation is
|
|
10
|
+
a GET or HEAD, outside the reserved prefixes, whose ``Accept`` names an HTML type: a browser
|
|
11
|
+
navigating always names one, and ``fetch`` defaults to ``*/*``, which is not one. Everything
|
|
12
|
+
else keeps the problem document. Precedence is unchanged by any of it: an API route wins,
|
|
13
|
+
then a real file, and the shell is the last resort.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from fastapi import FastAPI, HTTPException, Request, status
|
|
19
|
+
from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse
|
|
20
|
+
from starlette.responses import Response
|
|
21
|
+
from starlette.staticfiles import StaticFiles
|
|
22
|
+
from starlette.types import Scope
|
|
23
|
+
|
|
24
|
+
from dirigent_core import __version__
|
|
25
|
+
from dirigent_core.config import Settings
|
|
26
|
+
from dirigent_server.errors import http_error
|
|
27
|
+
|
|
28
|
+
#: The file whose presence means a bundle was built, rather than the directory existing.
|
|
29
|
+
INDEX_FILENAME = "index.html"
|
|
30
|
+
|
|
31
|
+
#: Where vite emits every content-hashed file, and the path they are served from.
|
|
32
|
+
ASSETS_MOUNT_PATH = "/assets"
|
|
33
|
+
|
|
34
|
+
CONFIG_PATH = "/config.json"
|
|
35
|
+
FAVICON_PATH = "/favicon.ico"
|
|
36
|
+
|
|
37
|
+
#: Icons the shell may reference, most specific first; the first one present answers
|
|
38
|
+
#: ``/favicon.ico``, which browsers ask for whatever the document declares.
|
|
39
|
+
FAVICON_FILENAMES = (("favicon.ico", "image/x-icon"), ("favicon.svg", "image/svg+xml"), ("favicon-32.png", "image/png"))
|
|
40
|
+
|
|
41
|
+
#: A content-hashed file's name changes whenever its bytes do, so it never goes stale.
|
|
42
|
+
IMMUTABLE = "public, max-age=31536000, immutable"
|
|
43
|
+
|
|
44
|
+
#: The shell names the current hashes, so a cached one loads files a rebuild deleted.
|
|
45
|
+
REVALIDATE = "no-cache"
|
|
46
|
+
|
|
47
|
+
#: The media types a navigating browser names, and a ``fetch`` default of ``*/*`` does not.
|
|
48
|
+
HTML_ACCEPT = ("text/html", "application/xhtml+xml")
|
|
49
|
+
|
|
50
|
+
#: Paths the shell must never answer for: a fetch that got HTML instead of JSON fails three
|
|
51
|
+
#: layers away, and a missing hashed asset answered with the page that asked for it loops.
|
|
52
|
+
RESERVED_PREFIXES = (
|
|
53
|
+
"/health",
|
|
54
|
+
"/hooks",
|
|
55
|
+
"/docs",
|
|
56
|
+
"/redoc",
|
|
57
|
+
"/openapi.json",
|
|
58
|
+
ASSETS_MOUNT_PATH,
|
|
59
|
+
CONFIG_PATH,
|
|
60
|
+
FAVICON_PATH,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
BUNDLE_MISSING = (
|
|
64
|
+
"The dirigent web UI is enabled and no bundle is built. Build one with `make ui`, "
|
|
65
|
+
"or set DIRIGENT_UI_ENABLED=false to run this instance as an API only. "
|
|
66
|
+
"The API itself is unaffected and is serving normally."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
#: Where the built bundle lives inside the installed package: vite's ``build.outDir``.
|
|
70
|
+
PACKAGED_STATIC = Path(__file__).resolve().parent / "static"
|
|
71
|
+
|
|
72
|
+
#: Where vite leaves it in a checkout, so the dev loop needs no install step.
|
|
73
|
+
CHECKOUT_STATIC = Path(__file__).resolve().parents[2] / "frontend" / "dist"
|
|
74
|
+
|
|
75
|
+
MOUNT_NAME = "ui"
|
|
76
|
+
ASSETS_MOUNT_NAME = "ui-assets"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class UiStaticFiles(StaticFiles):
|
|
80
|
+
"""Static files with a cache policy stated rather than left to a browser's heuristic.
|
|
81
|
+
|
|
82
|
+
Starlette sends only ETag and Last-Modified, and a response with no ``Cache-Control`` is
|
|
83
|
+
heuristically cacheable, so the policy is written down: the hashed asset tree is
|
|
84
|
+
immutable, and everything else revalidates.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, *, directory: Path, html: bool = False, immutable: bool = False) -> None:
|
|
88
|
+
"""Serve one directory, either as the hashed asset tree or as the shell's root.
|
|
89
|
+
|
|
90
|
+
The directory may not exist yet: a checkout serves the bundle a later ``make ui``
|
|
91
|
+
writes, so existence is a per-request question rather than a startup one.
|
|
92
|
+
"""
|
|
93
|
+
super().__init__(directory=directory, html=html, check_dir=False)
|
|
94
|
+
self.immutable = immutable
|
|
95
|
+
|
|
96
|
+
async def check_config(self) -> None:
|
|
97
|
+
"""Verify the directory only once it exists; before that, every path is a miss."""
|
|
98
|
+
if self.directory is not None and Path(self.directory).is_dir():
|
|
99
|
+
await super().check_config()
|
|
100
|
+
|
|
101
|
+
async def get_response(self, path: str, scope: Scope) -> Response:
|
|
102
|
+
"""Answer one static file, stating how long it may be held."""
|
|
103
|
+
response = await super().get_response(path, scope)
|
|
104
|
+
served = 200 <= response.status_code < 400
|
|
105
|
+
response.headers["cache-control"] = IMMUTABLE if self.immutable and served else REVALIDATE
|
|
106
|
+
return response
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def static_dir(settings: Settings) -> Path | None:
|
|
110
|
+
"""The built bundle this instance serves, or None when it has none to serve.
|
|
111
|
+
|
|
112
|
+
The installed package's own ``static/`` wins over a checkout's ``frontend/dist``.
|
|
113
|
+
"""
|
|
114
|
+
if not settings.ui_enabled:
|
|
115
|
+
return None
|
|
116
|
+
for candidate in (PACKAGED_STATIC, CHECKOUT_STATIC):
|
|
117
|
+
if (candidate / INDEX_FILENAME).is_file():
|
|
118
|
+
return candidate
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def serving_root(settings: Settings) -> Path | None:
|
|
123
|
+
"""Where this instance's bundle lives, or would land if one were built.
|
|
124
|
+
|
|
125
|
+
A bundle built later must be served without a restart, so the answer does not require
|
|
126
|
+
``index.html`` to exist yet: a directory a build writes into is enough. A built bundle
|
|
127
|
+
keeps the same precedence ``static_dir`` states; with none, a checkout's ``frontend/``
|
|
128
|
+
names where ``make ui`` will put one, and an installed package's own ``static/`` is where
|
|
129
|
+
a wheel would have carried one.
|
|
130
|
+
"""
|
|
131
|
+
if not settings.ui_enabled:
|
|
132
|
+
return None
|
|
133
|
+
built = static_dir(settings)
|
|
134
|
+
if built is not None:
|
|
135
|
+
return built
|
|
136
|
+
if CHECKOUT_STATIC.parent.is_dir():
|
|
137
|
+
return CHECKOUT_STATIC
|
|
138
|
+
if PACKAGED_STATIC.is_dir():
|
|
139
|
+
return PACKAGED_STATIC
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def mount_ui_assets(app: FastAPI, settings: Settings) -> None:
|
|
144
|
+
"""Register the UI's fixed paths, before every router: the asset tree and the config document."""
|
|
145
|
+
if not settings.ui_enabled:
|
|
146
|
+
return
|
|
147
|
+
_add_config_route(app, settings)
|
|
148
|
+
directory = serving_root(settings)
|
|
149
|
+
if directory is None:
|
|
150
|
+
return
|
|
151
|
+
app.mount(
|
|
152
|
+
ASSETS_MOUNT_PATH,
|
|
153
|
+
UiStaticFiles(directory=directory / ASSETS_MOUNT_PATH.lstrip("/"), immutable=True),
|
|
154
|
+
name=ASSETS_MOUNT_NAME,
|
|
155
|
+
)
|
|
156
|
+
_add_favicon_route(app, directory)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def mount_ui_shell(app: FastAPI, settings: Settings) -> None:
|
|
160
|
+
"""Mount the shell at ``/``, after every router, and answer a client route with it."""
|
|
161
|
+
if not settings.ui_enabled:
|
|
162
|
+
return
|
|
163
|
+
directory = serving_root(settings)
|
|
164
|
+
if directory is None:
|
|
165
|
+
_add_refusal_route(app)
|
|
166
|
+
return
|
|
167
|
+
app.mount("/", UiStaticFiles(directory=directory, html=True), name=MOUNT_NAME)
|
|
168
|
+
_add_shell_fallback(app, settings, directory / INDEX_FILENAME)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _add_config_route(app: FastAPI, settings: Settings) -> None:
|
|
172
|
+
"""Answer what the bundle cannot hardcode: where this instance's API is, and its version."""
|
|
173
|
+
|
|
174
|
+
@app.get(CONFIG_PATH, include_in_schema=False)
|
|
175
|
+
async def config() -> JSONResponse: # pyright: ignore[reportUnusedFunction]
|
|
176
|
+
return JSONResponse(
|
|
177
|
+
{"api_prefix": settings.api_prefix, "version": __version__},
|
|
178
|
+
headers={"cache-control": REVALIDATE},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _add_favicon_route(app: FastAPI, directory: Path) -> None:
|
|
183
|
+
"""Serve the first icon the bundle carries at ``/favicon.ico``."""
|
|
184
|
+
icons = [(directory / name, media_type) for name, media_type in FAVICON_FILENAMES]
|
|
185
|
+
|
|
186
|
+
@app.get(FAVICON_PATH, include_in_schema=False)
|
|
187
|
+
async def favicon() -> FileResponse: # pyright: ignore[reportUnusedFunction]
|
|
188
|
+
for path, media_type in icons:
|
|
189
|
+
if path.is_file():
|
|
190
|
+
return FileResponse(path, media_type=media_type, headers={"cache-control": REVALIDATE})
|
|
191
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="this bundle carries no icon")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _add_refusal_route(app: FastAPI) -> None:
|
|
195
|
+
"""Answer ``/`` with one sentence naming the command that builds the bundle."""
|
|
196
|
+
|
|
197
|
+
@app.get("/", include_in_schema=False)
|
|
198
|
+
async def missing() -> PlainTextResponse: # pyright: ignore[reportUnusedFunction]
|
|
199
|
+
return PlainTextResponse(
|
|
200
|
+
BUNDLE_MISSING,
|
|
201
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
202
|
+
headers={"cache-control": REVALIDATE},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _add_shell_fallback(app: FastAPI, settings: Settings, index: Path) -> None:
|
|
207
|
+
"""Serve the shell for a navigation nothing else claimed, and leave every other 404 alone.
|
|
208
|
+
|
|
209
|
+
The index is read per request, so a bundle built after startup is served without a
|
|
210
|
+
restart, and one a rebuild has momentarily emptied refuses honestly instead of erroring.
|
|
211
|
+
``/`` itself answers the refusal to any GET, navigation or not, because it is the one
|
|
212
|
+
path a person and a probe both try first.
|
|
213
|
+
"""
|
|
214
|
+
reserved = (settings.api_prefix, *RESERVED_PREFIXES)
|
|
215
|
+
|
|
216
|
+
async def shell_or_404(request: Request, error: Exception) -> Response:
|
|
217
|
+
at_root = request.url.path == "/" and request.method in ("GET", "HEAD")
|
|
218
|
+
if (_is_navigation(request) or at_root) and not request.url.path.startswith(reserved):
|
|
219
|
+
if index.is_file():
|
|
220
|
+
return FileResponse(index, media_type="text/html", headers={"cache-control": REVALIDATE})
|
|
221
|
+
if at_root or _is_navigation(request):
|
|
222
|
+
return PlainTextResponse(
|
|
223
|
+
BUNDLE_MISSING,
|
|
224
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
225
|
+
headers={"cache-control": REVALIDATE},
|
|
226
|
+
)
|
|
227
|
+
return await http_error(request, error)
|
|
228
|
+
|
|
229
|
+
app.add_exception_handler(status.HTTP_404_NOT_FOUND, shell_or_404)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_navigation(request: Request) -> bool:
|
|
233
|
+
"""Whether this request is a browser asking for a page, rather than code asking for data."""
|
|
234
|
+
if request.method not in ("GET", "HEAD"):
|
|
235
|
+
return False
|
|
236
|
+
accepted = request.headers.get("accept", "")
|
|
237
|
+
return any(media_type in accepted for media_type in HTML_ACCEPT)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dirigent-server
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Dirigent FastAPI server: the REST API, its authentication, and the health probes.
|
|
5
|
+
License-Expression: LicenseRef-Proprietary
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
8
|
+
Requires-Dist: dirigent-client
|
|
9
|
+
Requires-Dist: dirigent-common
|
|
10
|
+
Requires-Dist: dirigent-core
|
|
11
|
+
Requires-Dist: fastapi>=0.141.1
|
|
12
|
+
Requires-Python: >=3.13
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# dirigent-server
|
|
16
|
+
|
|
17
|
+
Dirigent FastAPI server: the REST API, its authentication, and the health probes.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
dirigent_server/__init__.py,sha256=4cI1TvtXRBRtawFFgbZy1F5R1FhjZjDdPughsO2leFw,318
|
|
2
|
+
dirigent_server/app.py,sha256=I5lbeBZwjiV9oUcqIxnm5KpMlBtOML_IsKuNwE_GGuA,6660
|
|
3
|
+
dirigent_server/dependencies.py,sha256=Ue_rXXlCntkN-2V12OU8qu9KqKI0WsDR_yIVObMxgws,1957
|
|
4
|
+
dirigent_server/errors.py,sha256=mMCLbJ-gZxF0WYnEs_QgF1XtS8Uwx5m_KS4soONEH0Q,4692
|
|
5
|
+
dirigent_server/health.py,sha256=HnS_YCKI5C_366oQNRg36Uwo6UOODLhOhlLy0wOsW9Y,3478
|
|
6
|
+
dirigent_server/logging.py,sha256=HNa30tPmos7yE_twoozvtHvtJXI-QB_HlY8Rpo6o_A4,223
|
|
7
|
+
dirigent_server/pagination.py,sha256=f9ur1wlhGCQj3c46Ck_S5dmhsh8WZrs9blfR_Zxytys,2074
|
|
8
|
+
dirigent_server/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
dirigent_server/routes/__init__.py,sha256=XOhUeN8m-xakoeN7SxFS8wp7Q8cvfOOXT5RE-7Bfwrc,2852
|
|
10
|
+
dirigent_server/routes/alerts.py,sha256=gs_q4bsneq61IO4SieTfpv2od0CQyNVWMzzATBb3stw,10694
|
|
11
|
+
dirigent_server/routes/auth.py,sha256=ctpyXOHm1QUN7URGE2stZ7-nSysxeBqTVx4UoHv1cBw,8557
|
|
12
|
+
dirigent_server/routes/blocks.py,sha256=xKp4tj4HYkv9VKZSmX6qUhfDhnx8zvGWYTKuk0nQUks,1514
|
|
13
|
+
dirigent_server/routes/connections.py,sha256=OcfLhVh1hIQ9CE8DVR-gAEFnq-N8cHJU5AanfbpogoU,12375
|
|
14
|
+
dirigent_server/routes/hooks.py,sha256=ImSiuZ8uDaj7cOxWJzrQwLK-uMJa1c6lta1sUnPjM_w,5323
|
|
15
|
+
dirigent_server/routes/pipelines.py,sha256=-s98ksFjdAbPN47N51hk9ukEKaPMWoiQvmNZcOfXLG4,16387
|
|
16
|
+
dirigent_server/routes/runs.py,sha256=zdjuaPI7OeZKATUg-EQT6DhoBpHF5tE2in_Qbmpl9uM,34486
|
|
17
|
+
dirigent_server/routes/schema.py,sha256=V0a-amIAiyQQTuW_K6X4stLH_OBiHpMFlbxXdPXFKrQ,1114
|
|
18
|
+
dirigent_server/routes/schemas.py,sha256=U80QuZUczj03vfKKlTWtoJ5nZ9z0-xSpIb6ltri7EP0,5461
|
|
19
|
+
dirigent_server/routes/system.py,sha256=Ah9HHcw8ULl59SllfzmyTZIn9yZy4F43nuwSrAug4oo,2397
|
|
20
|
+
dirigent_server/routes/trigger_documents.py,sha256=DLSmPtJJaQ-QG2kzPq-ACR0hIc6GS9KqDt8VovRFqh4,4225
|
|
21
|
+
dirigent_server/routes/triggers.py,sha256=sXIf7O_M3hqE1Rv-jBPDmI3-fYVnlSxw7Va5lHRDbgk,20446
|
|
22
|
+
dirigent_server/routes/users.py,sha256=PiBR9gZi3mISGpGPxSgmNUVxSjGx2msNY82NdqFvCh4,7849
|
|
23
|
+
dirigent_server/routes/workers.py,sha256=SvQMW_4SK0GFJ1SYdV8sT--gU7OOJXdL8wP0_tPH-t8,2059
|
|
24
|
+
dirigent_server/security.py,sha256=ZhocAlbIQ7UUpX0emSPWzoZUgoQZEf_xaR_esuKAsP0,7775
|
|
25
|
+
dirigent_server/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
dirigent_server/transactions.py,sha256=45fO6r3HFgXDfs9Bs2BgvHC1JYQ2nLiy2J--UlEGF4U,1459
|
|
27
|
+
dirigent_server/ui.py,sha256=YzHIxEjEX0wGsmXl2ffDWyZi72rMvi0y-KBGD9Raf3o,10234
|
|
28
|
+
dirigent_server-0.9.0.dist-info/licenses/LICENSE,sha256=LKBm7Cx-WBc1zca4DjGxq99VEpAiWGnZDxIKmntn1hQ,910
|
|
29
|
+
dirigent_server-0.9.0.dist-info/licenses/THIRD_PARTY_NOTICES.md,sha256=a_fvGwkpViiMt7XYjuiAvjNxt-6rTyVC2FYhrt5jjQQ,33423
|
|
30
|
+
dirigent_server-0.9.0.dist-info/WHEEL,sha256=mru_b36sH6joUMnwf7IlFCun3RoDjrNg9RcfBmEcqsE,81
|
|
31
|
+
dirigent_server-0.9.0.dist-info/METADATA,sha256=7pqewZxwVb4bLlXw3QJJwL43xj5Dj8mv5mJmJ8Q1LSU,541
|
|
32
|
+
dirigent_server-0.9.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright (c) 2026 Morten Olav Hansen <morten@winterop.com>. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This source code and accompanying documentation are the property of
|
|
4
|
+
Morten Olav Hansen. No license, express or implied, is granted to use, copy,
|
|
5
|
+
modify, merge, publish, distribute, sublicense, or sell copies of this
|
|
6
|
+
software or its derivatives.
|
|
7
|
+
|
|
8
|
+
The source is published for reference only. Any use beyond reading
|
|
9
|
+
requires written permission from the copyright holder.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
12
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
13
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
14
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES,
|
|
15
|
+
OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
|
|
16
|
+
|
|
17
|
+
Third-party components redistributed with this software, and the licences they
|
|
18
|
+
carry, are listed in THIRD_PARTY_NOTICES.md.
|