lightning-pose-app 1.8.1a2__py3-none-any.whl → 1.8.1a4__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.
Files changed (28) hide show
  1. lightning_pose_app-1.8.1a4.dist-info/METADATA +20 -0
  2. lightning_pose_app-1.8.1a4.dist-info/RECORD +29 -0
  3. litpose_app/config.py +22 -0
  4. litpose_app/deps.py +77 -0
  5. litpose_app/main.py +46 -188
  6. litpose_app/ngdist/ng_app/3rdpartylicenses.txt +11 -11
  7. litpose_app/ngdist/ng_app/index.html +3 -2
  8. litpose_app/ngdist/ng_app/{main-WFYIUX2C.js → main-VCJFCLFP.js} +184 -61
  9. litpose_app/ngdist/ng_app/main-VCJFCLFP.js.map +1 -0
  10. litpose_app/ngdist/ng_app/{styles-AJ6NQDUD.css → styles-ZM27COY6.css} +37 -7
  11. litpose_app/ngdist/ng_app/styles-ZM27COY6.css.map +7 -0
  12. litpose_app/ngdist/ng_app/video-tile.component-RDL4BSJ4.css.map +7 -0
  13. litpose_app/{run_ffprobe.py → routes/ffprobe.py} +34 -2
  14. litpose_app/{super_rglob.py → routes/files.py} +60 -0
  15. litpose_app/routes/project.py +72 -0
  16. litpose_app/routes/transcode.py +67 -0
  17. litpose_app/tasks/__init__.py +0 -0
  18. litpose_app/tasks/management.py +2 -0
  19. litpose_app/tasks/transcode_fine.py +7 -0
  20. litpose_app/transcode_fine.py +175 -0
  21. lightning_pose_app-1.8.1a2.dist-info/METADATA +0 -15
  22. lightning_pose_app-1.8.1a2.dist-info/RECORD +0 -20
  23. litpose_app/ngdist/ng_app/main-WFYIUX2C.js.map +0 -1
  24. litpose_app/ngdist/ng_app/styles-AJ6NQDUD.css.map +0 -7
  25. {lightning_pose_app-1.8.1a2.dist-info → lightning_pose_app-1.8.1a4.dist-info}/WHEEL +0 -0
  26. /litpose_app/ngdist/ng_app/{app.component-IZ5OUDH2.css.map → app.component-UAQUAGNZ.css.map} +0 -0
  27. /litpose_app/ngdist/ng_app/{project-settings.component-BXKZMYM3.css.map → project-settings.component-HKHIVUJR.css.map} +0 -0
  28. /litpose_app/ngdist/ng_app/{viewer-page.component-KIYG73MW.css.map → viewer-page.component-KDHT6XH5.css.map} +0 -0
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.3
2
+ Name: lightning-pose-app
3
+ Version: 1.8.1a4
4
+ Summary:
5
+ Requires-Python: >=3.10
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Provides-Extra: dev
12
+ Requires-Dist: fastapi (>=0.115.0,<0.116.0)
13
+ Requires-Dist: honcho (>=2.0,<3.0)
14
+ Requires-Dist: httpx (>=0.28.0,<0.29.0) ; extra == "dev"
15
+ Requires-Dist: pytest (>=8.4.0,<8.5.0) ; extra == "dev"
16
+ Requires-Dist: pytest-mock (>=3.14.0,<3.15.0) ; extra == "dev"
17
+ Requires-Dist: tomli (>=2.2.0,<2.3.0)
18
+ Requires-Dist: tomli_w (>=1.2.0,<1.3.0)
19
+ Requires-Dist: uvicorn[standard] (>=0.34.0,<0.35.0)
20
+ Requires-Dist: wcmatch (>=10.0,<11.0)
@@ -0,0 +1,29 @@
1
+ litpose_app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ litpose_app/config.py,sha256=6ZqU71kILT6q1IdYgvYKthYQa2cY9nMwgHeRih24F6s,861
3
+ litpose_app/deps.py,sha256=P0o762ufSW-9nqlpYPRtlrxpKBJMzPZ1LLfTDfTl-4U,2552
4
+ litpose_app/main.py,sha256=ftSNNRnToLN_DpRb_EfgarrxWhDmMsMX2fku4qNrzAU,5397
5
+ litpose_app/ngdist/ng_app/3rdpartylicenses.txt,sha256=8IgJBztx-L-9cLvY7pJB5PSXQJiBowGNUNV7knEAeq0,25355
6
+ litpose_app/ngdist/ng_app/app.component-UAQUAGNZ.css.map,sha256=e6lXWzmK3xppKK3tXHUccK1yGZqd1kzyTpDH0F1nC2g,344
7
+ litpose_app/ngdist/ng_app/error-dialog.component-HYLQSJEP.css.map,sha256=zJuF-LfB994Y1IrnIz38mariDFb8yucffbWPXgHGbvw,355
8
+ litpose_app/ngdist/ng_app/favicon.ico,sha256=QtbXVfx3HI-WqWh_kkBIQyYzJsDmLw_3Y4_UN7pBepE,15406
9
+ litpose_app/ngdist/ng_app/index.html,sha256=y1By3uv-f39NnrfG7hmIPhzC0f0FEDY0U70oqmU6NlQ,23004
10
+ litpose_app/ngdist/ng_app/main-VCJFCLFP.js,sha256=qKG_Ev1-OoVeTeqIVv9DKN8YmfIp2zn6-gBUNFVW__4,2725077
11
+ litpose_app/ngdist/ng_app/main-VCJFCLFP.js.map,sha256=f68Eh0pGhxR0Ol3rkdj4ifPljiw-rnv4JgFBWXsdDmA,5561146
12
+ litpose_app/ngdist/ng_app/prerendered-routes.json,sha256=p53cyKEVGQ6cGUee02kUdBp9HbdPChFTUp78gHJVBf4,18
13
+ litpose_app/ngdist/ng_app/project-settings.component-HKHIVUJR.css.map,sha256=v5tyba9p8ec3ZbHYyyUGTEFdEAsqT0l7JqtGRGjki6w,371
14
+ litpose_app/ngdist/ng_app/styles-ZM27COY6.css,sha256=3bpOGHzSsXAbzY4LgMt2d5xCLL0uY2HjdURJj1Rr3r0,69517
15
+ litpose_app/ngdist/ng_app/styles-ZM27COY6.css.map,sha256=w1T6js2TzjwM4XKfCmyGpfWeqkqzolJbll3wXCRl8Go,75027
16
+ litpose_app/ngdist/ng_app/video-player-controls.component-C4JZHYJ2.css.map,sha256=vX-dgeDCUCPLiea4Qy9O_EBm6IzzwB7R_uSBa0qU5Go,771
17
+ litpose_app/ngdist/ng_app/video-tile.component-RDL4BSJ4.css.map,sha256=_pZ7FxqOAu535JfrRv1TSKgRpDyQvn9Q0U39KHyJ980,332
18
+ litpose_app/ngdist/ng_app/viewer-page.component-KDHT6XH5.css.map,sha256=Uf1FgoCiF_qJpD4Ggk34Dq7kM73Q7i7NT6h3m30tbaY,211
19
+ litpose_app/routes/ffprobe.py,sha256=UVKDu4ghXkYNuzI-91KPMNpQukHV9Goeps7ex_lG_SU,5526
20
+ litpose_app/routes/files.py,sha256=HHyug226s_M4jMi8q7oI2dTWHTKu_J_c7vIbG0rdk9s,3102
21
+ litpose_app/routes/project.py,sha256=pQW7zFdGHMy_eE0Z1bIXs9QLkV1-_MwFHsZR51-H6uQ,1957
22
+ litpose_app/routes/transcode.py,sha256=9C4mMNgh0W7BHI2gdEnnoxMHVAoXag3eCXYhqOvUte4,2071
23
+ litpose_app/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ litpose_app/tasks/management.py,sha256=9wKT8i3CE8ueRCP3DpNd21UMy2_3Kn_LMLBp0dGStFc,120
25
+ litpose_app/tasks/transcode_fine.py,sha256=bX8OblwMO_xaXXqZXJmmQPprCYIpPGHiboyEJli6FHs,209
26
+ litpose_app/transcode_fine.py,sha256=zJKj1ozTSnmAEOddiqdy3ekRKdEAiNm2oMe9bcx520c,6142
27
+ lightning_pose_app-1.8.1a4.dist-info/METADATA,sha256=sIyFToit6PIpDi4jTpjALkGwPXPp-u0l6znOtkpfsIQ,793
28
+ lightning_pose_app-1.8.1a4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
29
+ lightning_pose_app-1.8.1a4.dist-info/RECORD,,
litpose_app/config.py ADDED
@@ -0,0 +1,22 @@
1
+ """Routes should not access this directly, if they want to be able to
2
+ modify these in unit tests.
3
+ Instead, prefer to inject `config: deps.config into the route using FastAPI's dependency injection.
4
+ See https://fastapi.tiangolo.com/tutorial/dependencies/."""
5
+
6
+ from pathlib import Path
7
+
8
+ from pydantic import BaseModel
9
+
10
+
11
+ # Consider `pydantic_settings.BaseSettings` for potential future needs.
12
+ class Config(BaseModel):
13
+ PROJECT_INFO_TOML_PATH: Path = Path("~/.lightning_pose/project.toml").expanduser()
14
+
15
+ ## Video transcoding settings
16
+
17
+ # Directory where finely transcoded videos are stored
18
+ FINE_VIDEO_DIR: Path = Path("~/.lightning_pose/finevideos").expanduser()
19
+
20
+ # We'll automatically transcode videos with size under this limit.
21
+ # Larger ones will have to be manually triggered (design TBD).
22
+ AUTO_TRANSCODE_VIDEO_SIZE_LIMIT_MB: int = 30
litpose_app/deps.py ADDED
@@ -0,0 +1,77 @@
1
+ """
2
+ Dependencies that can be injected into routes.
3
+ This has the benefit of making tests easier to write, as you can override dependencies.
4
+ See FastAPI Dependency Injection docs: https://fastapi.tiangolo.com/tutorial/dependencies/
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import math
11
+ import os
12
+ from typing import TYPE_CHECKING
13
+
14
+ from apscheduler.executors.debug import DebugExecutor
15
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
16
+ from apscheduler.executors.pool import ThreadPoolExecutor
17
+ from fastapi import Depends
18
+
19
+ from litpose_app.config import Config
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def config() -> Config:
25
+ """Dependency that provides the app config object."""
26
+ from .main import app
27
+
28
+ if not hasattr(app.state, "config"):
29
+ app.state.config = Config()
30
+ return app.state.config
31
+
32
+
33
+ def scheduler() -> AsyncIOScheduler:
34
+ """Dependency that provides the app's APScheduler instance."""
35
+ from .main import app
36
+
37
+ if not hasattr(app.state, "scheduler"):
38
+ # ffmpeg parallelizes transcoding to the optimal degree, but
39
+ # that doesn't always saturate a machine with a lot of cores.
40
+ # i.e. on a 24 logical core machine (12 physical * 2 hyperthreads per core)
41
+ # 3 was the ideal number of max_workers. Let's just guesstimate that
42
+ # ffmpeg uses 10 cores? No scientific evidence, but ceil(24/10) => 3.
43
+ transcode_workers = math.ceil(os.cpu_count() / 10)
44
+ executors = {
45
+ "transcode_pool": ThreadPoolExecutor(max_workers=transcode_workers),
46
+ "debug": DebugExecutor(),
47
+ }
48
+ app.state.scheduler = AsyncIOScheduler(executors=executors)
49
+ return app.state.scheduler
50
+
51
+
52
+ if TYPE_CHECKING:
53
+ from .routes.project import ProjectInfo
54
+
55
+
56
+ def project_info(config: Config = Depends(config)) -> ProjectInfo:
57
+ import tomli
58
+ from .routes.project import ProjectInfo
59
+
60
+ from pydantic import ValidationError
61
+
62
+ try:
63
+ # Open the file in binary read mode, as recommended by tomli
64
+ with open(config.PROJECT_INFO_TOML_PATH, "rb") as f:
65
+ # Load the TOML data into a Python dictionary
66
+ toml_data = tomli.load(f)
67
+
68
+ # Unpack the dictionary into the Pydantic model
69
+ return ProjectInfo(**toml_data)
70
+ except FileNotFoundError:
71
+ return None
72
+ except tomli.TOMLDecodeError as e:
73
+ logger.error(f"Could not decode pyproject.toml. Invalid syntax: {e}")
74
+ raise
75
+ except ValidationError as e:
76
+ logger.error(f"pyproject.toml is invalid. {e}")
77
+ raise
litpose_app/main.py CHANGED
@@ -1,24 +1,53 @@
1
+ import logging
2
+ import sys
3
+ from contextlib import asynccontextmanager
1
4
  from pathlib import Path
2
5
  from textwrap import dedent
3
6
 
4
- import tomli
5
- import tomli_w
6
- from fastapi import FastAPI, HTTPException
7
- from fastapi.responses import FileResponse
8
- import sys
9
7
  import uvicorn
10
- from pydantic import BaseModel, ValidationError
8
+ from fastapi import FastAPI, HTTPException, APIRouter, Request
9
+ from fastapi.responses import FileResponse
11
10
  from starlette import status
12
- from starlette.requests import Request
13
11
  from starlette.responses import Response
14
12
  from starlette.staticfiles import StaticFiles
15
13
 
16
- from .run_ffprobe import run_ffprobe
17
- from .super_rglob import super_rglob
14
+ from . import deps
15
+
16
+ ## Setup logging
17
+ logging.basicConfig(
18
+ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
19
+ )
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ ## Configure additional things to happen on server startup and shutdown.
24
+ @asynccontextmanager
25
+ async def lifespan(app: FastAPI):
26
+ # Start apscheduler, which is responsible for executing background tasks
27
+ logger.info("Application startup: Initializing scheduler...")
28
+ scheduler = deps.scheduler()
29
+ scheduler.start()
30
+
31
+ yield # Application is now ready to receive requests
32
+
33
+ logger.info("Application shutdown: Shutting down scheduler...")
34
+ if scheduler and scheduler.running:
35
+ scheduler.shutdown()
36
+ logger.info("Scheduler shut down.")
37
+ else:
38
+ logger.warning("Scheduler not found or not running during shutdown.")
18
39
 
19
- # use this example to pull useful features from:
20
- # https://github.com/fastapi/full-stack-fastapi-template
21
- app = FastAPI()
40
+
41
+ app = FastAPI(lifespan=lifespan)
42
+
43
+ router = APIRouter()
44
+ from .routes import ffprobe, files, project, transcode
45
+
46
+ router.include_router(ffprobe.router)
47
+ router.include_router(files.router)
48
+ router.include_router(project.router)
49
+ router.include_router(transcode.router)
50
+ app.include_router(router)
22
51
 
23
52
 
24
53
  @app.exception_handler(Exception)
@@ -41,17 +70,6 @@ async def debug_exception_handler(request: Request, exc: Exception):
41
70
  )
42
71
 
43
72
 
44
- PROJECT_INFO_TOML_PATH = Path("~/.lightning_pose/project.toml").expanduser()
45
-
46
-
47
- class ProjectInfo(BaseModel):
48
- """Class to hold information about the project"""
49
-
50
- data_dir: Path | None = None
51
- model_dir: Path | None = None
52
- views: list[str] | None = None
53
-
54
-
55
73
  """
56
74
  All our methods are RPC style (http url corresponds to method name).
57
75
  They should be POST requests, /rpc/<method_name>.
@@ -64,160 +82,9 @@ error in a dialog to the user. So if the client is supposed to
64
82
  handle the error in any way, for example, special form validation UX
65
83
  like underlining the invalid field,
66
84
  then the information about the error should be included in a valid
67
- response object rather than raised as a python error.
85
+ response object rather than raised as a python error.
68
86
  """
69
87
 
70
-
71
- class GetProjectInfoResponse(BaseModel):
72
- projectInfo: ProjectInfo | None # None if project info not yet initialized
73
-
74
-
75
- @app.post("/app/v0/rpc/getProjectInfo")
76
- def get_project_info() -> GetProjectInfoResponse:
77
- try:
78
- # Open the file in binary read mode, as recommended by tomli
79
- with open(PROJECT_INFO_TOML_PATH, "rb") as f:
80
- # Load the TOML data into a Python dictionary
81
- toml_data = tomli.load(f)
82
-
83
- # Unpack the dictionary into the Pydantic model
84
- # Pydantic will handle all the validation from here.
85
- obj = ProjectInfo(**toml_data)
86
- return GetProjectInfoResponse(projectInfo=obj)
87
-
88
- except FileNotFoundError:
89
- return GetProjectInfoResponse(projectInfo=None)
90
- except tomli.TOMLDecodeError as e:
91
- print(f"Error: Could not decode the TOML file. Invalid syntax: {e}")
92
- raise
93
- except ValidationError as e:
94
- # Pydantic's validation error is very informative
95
- print(f"Error: Configuration is invalid. {e}")
96
- raise
97
-
98
-
99
- class SetProjectInfoRequest(BaseModel):
100
- projectInfo: ProjectInfo
101
-
102
-
103
- @app.post("/app/v0/rpc/setProjectInfo")
104
- def set_project_info(request: SetProjectInfoRequest) -> None:
105
- try:
106
- PROJECT_INFO_TOML_PATH.parent.mkdir(parents=True, exist_ok=True)
107
-
108
- # Convert the Pydantic model to a dictionary for TOML serialization.
109
- # Use mode=json to make the resulting dict json-serializable (and thus
110
- # also toml serializable)
111
- project_data_dict = request.projectInfo.model_dump(
112
- mode="json", exclude_none=True
113
- )
114
- try:
115
- with open(PROJECT_INFO_TOML_PATH, "rb") as f:
116
- existing_project_data = tomli.load(f)
117
- except FileNotFoundError:
118
- existing_project_data = {}
119
-
120
- # Apply changes onto existing data, i.e. PATCH semantics.
121
- existing_project_data.update(project_data_dict)
122
-
123
- # Open the file in binary write mode to write the TOML data
124
- with open(PROJECT_INFO_TOML_PATH, "wb") as f:
125
- tomli_w.dump(existing_project_data, f)
126
-
127
- return None
128
-
129
- except IOError as e:
130
- # This catches errors related to file operations (e.g., permissions, disk full)
131
- error_message = f"Failed to write project information to file: {str(e)}"
132
- print(error_message) # Log server-side
133
- raise e
134
- except Exception as e: # Catch any other unexpected errors
135
- error_message = (
136
- f"An unexpected error occurred while saving project info: {str(e)}"
137
- )
138
- print(error_message) # Log server-side
139
- raise e
140
-
141
-
142
- class RGlobRequest(BaseModel):
143
- baseDir: Path
144
- pattern: str
145
- noDirs: bool = False
146
- stat: bool = False
147
-
148
-
149
- class RGlobResponseEntry(BaseModel):
150
- path: Path
151
-
152
- # Present only if request had stat=True or noDirs=True
153
- type: str | None
154
-
155
- # Present only if request had stat=True
156
-
157
- size: int | None
158
- # Creation timestamp, ISO format.
159
- cTime: str | None
160
- # Modified timestamp, ISO format.
161
- mTime: str | None
162
-
163
-
164
- class RGlobResponse(BaseModel):
165
- entries: list[RGlobResponseEntry]
166
- relativeTo: Path # this is going to be the same base_dir that was in the request.
167
-
168
-
169
- @app.post("/app/v0/rpc/rglob")
170
- def rglob(request: RGlobRequest) -> RGlobResponse:
171
- # Prevent secrets like /etc/passwd and ~/.ssh/ from being leaked.
172
- if not (request.pattern.endswith(".csv") or request.pattern.endswith(".mp4")):
173
- raise HTTPException(
174
- status_code=status.HTTP_403_FORBIDDEN,
175
- detail="Only csv and mp4 files are supported.",
176
- )
177
-
178
- response = RGlobResponse(entries=[], relativeTo=request.baseDir)
179
-
180
- results = super_rglob(
181
- str(request.baseDir),
182
- pattern=request.pattern,
183
- no_dirs=request.noDirs,
184
- stat=request.stat,
185
- )
186
- for r in results:
187
- # Convert dict to pydantic model
188
- converted = RGlobResponseEntry.model_validate(r)
189
- response.entries.append(converted)
190
-
191
- return response
192
-
193
-
194
- class FFProbeRequest(BaseModel):
195
- path: Path
196
-
197
-
198
- class FFProbeResponse(BaseModel):
199
- codec: str
200
- width: int
201
- height: int
202
- fps: int
203
- duration: float
204
-
205
-
206
- @app.post("/app/v0/rpc/ffprobe")
207
- def ffprobe(request: FFProbeRequest) -> FFProbeResponse:
208
- if request.path.suffix != ".mp4":
209
- raise HTTPException(
210
- status_code=status.HTTP_403_FORBIDDEN,
211
- detail="Only mp4 files are supported.",
212
- )
213
-
214
- result = run_ffprobe(str(request.path))
215
-
216
- response = FFProbeResponse.model_validate(result)
217
-
218
- return response
219
-
220
-
221
88
  """
222
89
  File server to serve csv and video files.
223
90
  FileResponse supports range requests for video buffering.
@@ -251,11 +118,12 @@ def read_file(file_path: Path):
251
118
  # This is necessary to use HTTP2 for faster concurrent request performance (ng serve doesn't support it).
252
119
  ###########################################################################
253
120
 
121
+
254
122
  # Serve ng assets (js, css)
255
123
  STATIC_DIR = Path(__file__).parent / "ngdist" / "ng_app"
256
124
  if not STATIC_DIR.is_dir():
257
125
  message = dedent(
258
- f"""
126
+ """
259
127
  ⚠️ Warning: We couldn't find the necessary static assets (like HTML, CSS, JavaScript files).
260
128
  As a result, only the HTTP API is currently running.
261
129
 
@@ -276,25 +144,15 @@ app.mount("/static", StaticFiles(directory=STATIC_DIR, check_dir=False), name="s
276
144
 
277
145
 
278
146
  @app.get("/favicon.ico")
279
- def favicon():
147
+ async def favicon():
280
148
  return FileResponse(Path(__file__).parent / "ngdist" / "ng_app" / "favicon.ico")
281
149
 
282
150
 
283
151
  # Catch-all route. serve index.html.
284
152
  @app.get("/{full_path:path}")
285
- def index(full_path: Path):
153
+ async def index():
286
154
  return FileResponse(Path(__file__).parent / "ngdist" / "ng_app" / "index.html")
287
155
 
288
156
 
289
- def get_static_files_if_needed():
290
- cache_dir = Path("~/.lightning_pose/cache").expanduser()
291
- # Version check
292
- # App should run with "latest compatible version"
293
- # this means that if lightning pose is installed, it gets the latest version compatible with that version.
294
- # otherwise it gets just the latest version.
295
- # Download the files?
296
-
297
-
298
157
  def run_app(host: str, port: int):
299
- get_static_files_if_needed()
300
158
  uvicorn.run(app, host=host, port=port)
@@ -341,17 +341,17 @@ License: "Apache-2.0"
341
341
  Package: tslib
342
342
  License: "0BSD"
343
343
 
344
- Copyright (c) Microsoft Corporation.
345
-
346
- Permission to use, copy, modify, and/or distribute this software for any
347
- purpose with or without fee is hereby granted.
348
-
349
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
350
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
351
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
352
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
353
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
354
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
344
+ Copyright (c) Microsoft Corporation.
345
+
346
+ Permission to use, copy, modify, and/or distribute this software for any
347
+ purpose with or without fee is hereby granted.
348
+
349
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
350
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
351
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
352
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
353
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
354
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
355
355
  PERFORMANCE OF THIS SOFTWARE.
356
356
  --------------------------------------------------------------------------------
357
357
  Package: @angular/common
@@ -329,6 +329,7 @@
329
329
  --color-green-400: oklch(79.2% 0.209 151.711);
330
330
  --color-sky-100: oklch(95.1% 0.026 236.824);
331
331
  --color-sky-700: oklch(50% 0.134 242.749);
332
+ --color-gray-400: oklch(70.7% 0.022 261.325);
332
333
  --color-black: #000;
333
334
  --spacing: 0.25rem;
334
335
  --container-xs: 20rem;
@@ -444,8 +445,8 @@
444
445
  }
445
446
  }
446
447
  }
447
- </style><link rel="stylesheet" href="/static/styles-AJ6NQDUD.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="/static/styles-AJ6NQDUD.css"></noscript></head>
448
+ </style><link rel="stylesheet" href="/static/styles-ZM27COY6.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="/static/styles-ZM27COY6.css"></noscript></head>
448
449
  <body>
449
450
  <app-root></app-root>
450
- <script src="/static/main-WFYIUX2C.js" type="module"></script></body>
451
+ <script src="/static/main-VCJFCLFP.js" type="module"></script></body>
451
452
  </html>