lightning-pose-app 1.8.1a2__py3-none-any.whl → 1.8.1a3__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.
- {lightning_pose_app-1.8.1a2.dist-info → lightning_pose_app-1.8.1a3.dist-info}/METADATA +1 -1
- lightning_pose_app-1.8.1a3.dist-info/RECORD +21 -0
- litpose_app/main.py +300 -300
- litpose_app/ngdist/ng_app/index.html +2 -2
- litpose_app/ngdist/ng_app/{main-WFYIUX2C.js → main-LJHMLKBL.js} +199 -149
- litpose_app/ngdist/ng_app/main-LJHMLKBL.js.map +1 -0
- litpose_app/ngdist/ng_app/{styles-AJ6NQDUD.css → styles-4V6RXJMC.css} +27 -7
- litpose_app/ngdist/ng_app/{styles-AJ6NQDUD.css.map → styles-4V6RXJMC.css.map} +2 -2
- litpose_app/ngdist/ng_app/video-tile.component-XSYKMARQ.css.map +7 -0
- litpose_app/run_ffprobe.py +132 -132
- litpose_app/super_rglob.py +48 -48
- lightning_pose_app-1.8.1a2.dist-info/RECORD +0 -20
- litpose_app/ngdist/ng_app/main-WFYIUX2C.js.map +0 -1
- {lightning_pose_app-1.8.1a2.dist-info → lightning_pose_app-1.8.1a3.dist-info}/WHEEL +0 -0
- /litpose_app/ngdist/ng_app/{app.component-IZ5OUDH2.css.map → app.component-UHVEDPZR.css.map} +0 -0
- /litpose_app/ngdist/ng_app/{project-settings.component-BXKZMYM3.css.map → project-settings.component-5IRK7U7U.css.map} +0 -0
- /litpose_app/ngdist/ng_app/{viewer-page.component-KIYG73MW.css.map → viewer-page.component-MRTIUFL2.css.map} +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
litpose_app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
litpose_app/main.py,sha256=ZjD5i_zUVbSxsHPEn6J2TEtPTCEAdtqP5_-DndakixY,10037
|
|
3
|
+
litpose_app/ngdist/ng_app/3rdpartylicenses.txt,sha256=k34Q8jZikvFWsgFXPlQxFJVcqMXDEiKKzC_VIEK8jNs,25366
|
|
4
|
+
litpose_app/ngdist/ng_app/app.component-UHVEDPZR.css.map,sha256=e6lXWzmK3xppKK3tXHUccK1yGZqd1kzyTpDH0F1nC2g,344
|
|
5
|
+
litpose_app/ngdist/ng_app/error-dialog.component-HYLQSJEP.css.map,sha256=zJuF-LfB994Y1IrnIz38mariDFb8yucffbWPXgHGbvw,355
|
|
6
|
+
litpose_app/ngdist/ng_app/favicon.ico,sha256=QtbXVfx3HI-WqWh_kkBIQyYzJsDmLw_3Y4_UN7pBepE,15406
|
|
7
|
+
litpose_app/ngdist/ng_app/index.html,sha256=ATacgBvjlF3gLRzFCtDnS3shRzTOp8Rzu4rKfltBDOQ,22954
|
|
8
|
+
litpose_app/ngdist/ng_app/main-LJHMLKBL.js,sha256=nXrPvnVQwhHQJdHMsF-41nu5Mu7K6WARxWSei2g0y58,2722143
|
|
9
|
+
litpose_app/ngdist/ng_app/main-LJHMLKBL.js.map,sha256=ltcuu1XsSXJv0p_KjbUSPBKi9zyAy4sVQXmJxZWNkos,5557811
|
|
10
|
+
litpose_app/ngdist/ng_app/prerendered-routes.json,sha256=p53cyKEVGQ6cGUee02kUdBp9HbdPChFTUp78gHJVBf4,18
|
|
11
|
+
litpose_app/ngdist/ng_app/project-settings.component-5IRK7U7U.css.map,sha256=v5tyba9p8ec3ZbHYyyUGTEFdEAsqT0l7JqtGRGjki6w,371
|
|
12
|
+
litpose_app/ngdist/ng_app/styles-4V6RXJMC.css,sha256=mZ4r-BPkV1KTv3FvvjCJWuUygILiOzl8Jzjesyo0P8U,69300
|
|
13
|
+
litpose_app/ngdist/ng_app/styles-4V6RXJMC.css.map,sha256=Fm7m4Uakz0HLL1DMEyfCAAXoFia46_3Eab27wowU9tU,74911
|
|
14
|
+
litpose_app/ngdist/ng_app/video-player-controls.component-C4JZHYJ2.css.map,sha256=vX-dgeDCUCPLiea4Qy9O_EBm6IzzwB7R_uSBa0qU5Go,771
|
|
15
|
+
litpose_app/ngdist/ng_app/video-tile.component-XSYKMARQ.css.map,sha256=_pZ7FxqOAu535JfrRv1TSKgRpDyQvn9Q0U39KHyJ980,332
|
|
16
|
+
litpose_app/ngdist/ng_app/viewer-page.component-MRTIUFL2.css.map,sha256=Uf1FgoCiF_qJpD4Ggk34Dq7kM73Q7i7NT6h3m30tbaY,211
|
|
17
|
+
litpose_app/run_ffprobe.py,sha256=7acBcSJ-3t8N_6HZ2KrTrMkivPsxKlGuYzI6yNhaXEs,4975
|
|
18
|
+
litpose_app/super_rglob.py,sha256=kgSIra2xznEHhi9kBUzO0SpBb6PkUJCct0WSS33bRmM,1619
|
|
19
|
+
lightning_pose_app-1.8.1a3.dist-info/METADATA,sha256=2JCRkaIImYE4r6Y-UcBvGGe7tkRfXlGH2X2PlrMih7A,473
|
|
20
|
+
lightning_pose_app-1.8.1a3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
21
|
+
lightning_pose_app-1.8.1a3.dist-info/RECORD,,
|
litpose_app/main.py
CHANGED
|
@@ -1,300 +1,300 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from textwrap import dedent
|
|
3
|
-
|
|
4
|
-
import tomli
|
|
5
|
-
import tomli_w
|
|
6
|
-
from fastapi import FastAPI, HTTPException
|
|
7
|
-
from fastapi.responses import FileResponse
|
|
8
|
-
import sys
|
|
9
|
-
import uvicorn
|
|
10
|
-
from pydantic import BaseModel, ValidationError
|
|
11
|
-
from starlette import status
|
|
12
|
-
from starlette.requests import Request
|
|
13
|
-
from starlette.responses import Response
|
|
14
|
-
from starlette.staticfiles import StaticFiles
|
|
15
|
-
|
|
16
|
-
from .run_ffprobe import run_ffprobe
|
|
17
|
-
from .super_rglob import super_rglob
|
|
18
|
-
|
|
19
|
-
# use this example to pull useful features from:
|
|
20
|
-
# https://github.com/fastapi/full-stack-fastapi-template
|
|
21
|
-
app = FastAPI()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@app.exception_handler(Exception)
|
|
25
|
-
async def debug_exception_handler(request: Request, exc: Exception):
|
|
26
|
-
"""Puts error stack trace in response when any server exception occurs.
|
|
27
|
-
|
|
28
|
-
By default, FastAPI returns 500 "internal server error" on any Exception
|
|
29
|
-
that is not a subclass of HttpException. This is usually recommended in production apps.
|
|
30
|
-
|
|
31
|
-
In our app, it's more convenient to expose exception details to the user. The
|
|
32
|
-
security risk is minimal."""
|
|
33
|
-
import traceback
|
|
34
|
-
|
|
35
|
-
return Response(
|
|
36
|
-
status_code=500,
|
|
37
|
-
content="".join(
|
|
38
|
-
traceback.format_exception(type(exc), value=exc, tb=exc.__traceback__)
|
|
39
|
-
),
|
|
40
|
-
headers={"Content-Type": "text/plain"},
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
"""
|
|
56
|
-
All our methods are RPC style (http url corresponds to method name).
|
|
57
|
-
They should be POST requests, /rpc/<method_name>.
|
|
58
|
-
Request body is some object (pydantic model).
|
|
59
|
-
Response body is some object pydantic model.
|
|
60
|
-
|
|
61
|
-
The client expects all RPC methods to succeed. If any RPC doesn't
|
|
62
|
-
return the expected response object, it will be shown as an
|
|
63
|
-
error in a dialog to the user. So if the client is supposed to
|
|
64
|
-
handle the error in any way, for example, special form validation UX
|
|
65
|
-
like underlining the invalid field,
|
|
66
|
-
then the information about the error should be included in a valid
|
|
67
|
-
response object rather than raised as a python error.
|
|
68
|
-
"""
|
|
69
|
-
|
|
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
|
-
"""
|
|
222
|
-
File server to serve csv and video files.
|
|
223
|
-
FileResponse supports range requests for video buffering.
|
|
224
|
-
For security - only supports reading out of data_dir and model_dir
|
|
225
|
-
If we need to read out of other directories, they should be added to Project Info.
|
|
226
|
-
"""
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
@app.get("/app/v0/files/{file_path:path}")
|
|
230
|
-
def read_file(file_path: Path):
|
|
231
|
-
# Prevent secrets like /etc/passwd and ~/.ssh/ from being leaked.
|
|
232
|
-
if file_path.suffix not in (".csv", ".mp4"):
|
|
233
|
-
raise HTTPException(
|
|
234
|
-
status_code=status.HTTP_403_FORBIDDEN,
|
|
235
|
-
detail="Only csv and mp4 files are supported.",
|
|
236
|
-
)
|
|
237
|
-
file_path = Path("/") / file_path
|
|
238
|
-
|
|
239
|
-
# Only capable of returning files that exist (not directories).
|
|
240
|
-
if not file_path.is_file():
|
|
241
|
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
242
|
-
|
|
243
|
-
return FileResponse(file_path)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
###########################################################################
|
|
247
|
-
# Serving angular
|
|
248
|
-
#
|
|
249
|
-
# In dev mode, `ng serve` serves ng, and proxies to us for backend requests.
|
|
250
|
-
# In production mode, we will serve ng.
|
|
251
|
-
# This is necessary to use HTTP2 for faster concurrent request performance (ng serve doesn't support it).
|
|
252
|
-
###########################################################################
|
|
253
|
-
|
|
254
|
-
# Serve ng assets (js, css)
|
|
255
|
-
STATIC_DIR = Path(__file__).parent / "ngdist" / "ng_app"
|
|
256
|
-
if not STATIC_DIR.is_dir():
|
|
257
|
-
message = dedent(
|
|
258
|
-
f"""
|
|
259
|
-
⚠️ Warning: We couldn't find the necessary static assets (like HTML, CSS, JavaScript files).
|
|
260
|
-
As a result, only the HTTP API is currently running.
|
|
261
|
-
|
|
262
|
-
This usually happens if you've cloned the source code directly.
|
|
263
|
-
To fix this and get the full application working, you'll need to either:
|
|
264
|
-
|
|
265
|
-
- Build the application: Refer to development.md in the repository for steps.
|
|
266
|
-
- Copy static files: Obtain these files from a PyPI source distribution of a released
|
|
267
|
-
version and place them in:
|
|
268
|
-
|
|
269
|
-
{STATIC_DIR}
|
|
270
|
-
"""
|
|
271
|
-
)
|
|
272
|
-
# print(f'{Fore.white}{Back.yellow}{message}{Style.reset}', file=sys.stderr)
|
|
273
|
-
print(f"{message}", file=sys.stderr)
|
|
274
|
-
|
|
275
|
-
app.mount("/static", StaticFiles(directory=STATIC_DIR, check_dir=False), name="static")
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
@app.get("/favicon.ico")
|
|
279
|
-
def favicon():
|
|
280
|
-
return FileResponse(Path(__file__).parent / "ngdist" / "ng_app" / "favicon.ico")
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
# Catch-all route. serve index.html.
|
|
284
|
-
@app.get("/{full_path:path}")
|
|
285
|
-
def index(full_path: Path):
|
|
286
|
-
return FileResponse(Path(__file__).parent / "ngdist" / "ng_app" / "index.html")
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
def run_app(host: str, port: int):
|
|
299
|
-
get_static_files_if_needed()
|
|
300
|
-
uvicorn.run(app, host=host, port=port)
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from textwrap import dedent
|
|
3
|
+
|
|
4
|
+
import tomli
|
|
5
|
+
import tomli_w
|
|
6
|
+
from fastapi import FastAPI, HTTPException
|
|
7
|
+
from fastapi.responses import FileResponse
|
|
8
|
+
import sys
|
|
9
|
+
import uvicorn
|
|
10
|
+
from pydantic import BaseModel, ValidationError
|
|
11
|
+
from starlette import status
|
|
12
|
+
from starlette.requests import Request
|
|
13
|
+
from starlette.responses import Response
|
|
14
|
+
from starlette.staticfiles import StaticFiles
|
|
15
|
+
|
|
16
|
+
from .run_ffprobe import run_ffprobe
|
|
17
|
+
from .super_rglob import super_rglob
|
|
18
|
+
|
|
19
|
+
# use this example to pull useful features from:
|
|
20
|
+
# https://github.com/fastapi/full-stack-fastapi-template
|
|
21
|
+
app = FastAPI()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.exception_handler(Exception)
|
|
25
|
+
async def debug_exception_handler(request: Request, exc: Exception):
|
|
26
|
+
"""Puts error stack trace in response when any server exception occurs.
|
|
27
|
+
|
|
28
|
+
By default, FastAPI returns 500 "internal server error" on any Exception
|
|
29
|
+
that is not a subclass of HttpException. This is usually recommended in production apps.
|
|
30
|
+
|
|
31
|
+
In our app, it's more convenient to expose exception details to the user. The
|
|
32
|
+
security risk is minimal."""
|
|
33
|
+
import traceback
|
|
34
|
+
|
|
35
|
+
return Response(
|
|
36
|
+
status_code=500,
|
|
37
|
+
content="".join(
|
|
38
|
+
traceback.format_exception(type(exc), value=exc, tb=exc.__traceback__)
|
|
39
|
+
),
|
|
40
|
+
headers={"Content-Type": "text/plain"},
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
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
|
+
"""
|
|
56
|
+
All our methods are RPC style (http url corresponds to method name).
|
|
57
|
+
They should be POST requests, /rpc/<method_name>.
|
|
58
|
+
Request body is some object (pydantic model).
|
|
59
|
+
Response body is some object pydantic model.
|
|
60
|
+
|
|
61
|
+
The client expects all RPC methods to succeed. If any RPC doesn't
|
|
62
|
+
return the expected response object, it will be shown as an
|
|
63
|
+
error in a dialog to the user. So if the client is supposed to
|
|
64
|
+
handle the error in any way, for example, special form validation UX
|
|
65
|
+
like underlining the invalid field,
|
|
66
|
+
then the information about the error should be included in a valid
|
|
67
|
+
response object rather than raised as a python error.
|
|
68
|
+
"""
|
|
69
|
+
|
|
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
|
+
"""
|
|
222
|
+
File server to serve csv and video files.
|
|
223
|
+
FileResponse supports range requests for video buffering.
|
|
224
|
+
For security - only supports reading out of data_dir and model_dir
|
|
225
|
+
If we need to read out of other directories, they should be added to Project Info.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@app.get("/app/v0/files/{file_path:path}")
|
|
230
|
+
def read_file(file_path: Path):
|
|
231
|
+
# Prevent secrets like /etc/passwd and ~/.ssh/ from being leaked.
|
|
232
|
+
if file_path.suffix not in (".csv", ".mp4"):
|
|
233
|
+
raise HTTPException(
|
|
234
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
235
|
+
detail="Only csv and mp4 files are supported.",
|
|
236
|
+
)
|
|
237
|
+
file_path = Path("/") / file_path
|
|
238
|
+
|
|
239
|
+
# Only capable of returning files that exist (not directories).
|
|
240
|
+
if not file_path.is_file():
|
|
241
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
242
|
+
|
|
243
|
+
return FileResponse(file_path)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
###########################################################################
|
|
247
|
+
# Serving angular
|
|
248
|
+
#
|
|
249
|
+
# In dev mode, `ng serve` serves ng, and proxies to us for backend requests.
|
|
250
|
+
# In production mode, we will serve ng.
|
|
251
|
+
# This is necessary to use HTTP2 for faster concurrent request performance (ng serve doesn't support it).
|
|
252
|
+
###########################################################################
|
|
253
|
+
|
|
254
|
+
# Serve ng assets (js, css)
|
|
255
|
+
STATIC_DIR = Path(__file__).parent / "ngdist" / "ng_app"
|
|
256
|
+
if not STATIC_DIR.is_dir():
|
|
257
|
+
message = dedent(
|
|
258
|
+
f"""
|
|
259
|
+
⚠️ Warning: We couldn't find the necessary static assets (like HTML, CSS, JavaScript files).
|
|
260
|
+
As a result, only the HTTP API is currently running.
|
|
261
|
+
|
|
262
|
+
This usually happens if you've cloned the source code directly.
|
|
263
|
+
To fix this and get the full application working, you'll need to either:
|
|
264
|
+
|
|
265
|
+
- Build the application: Refer to development.md in the repository for steps.
|
|
266
|
+
- Copy static files: Obtain these files from a PyPI source distribution of a released
|
|
267
|
+
version and place them in:
|
|
268
|
+
|
|
269
|
+
{STATIC_DIR}
|
|
270
|
+
"""
|
|
271
|
+
)
|
|
272
|
+
# print(f'{Fore.white}{Back.yellow}{message}{Style.reset}', file=sys.stderr)
|
|
273
|
+
print(f"{message}", file=sys.stderr)
|
|
274
|
+
|
|
275
|
+
app.mount("/static", StaticFiles(directory=STATIC_DIR, check_dir=False), name="static")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@app.get("/favicon.ico")
|
|
279
|
+
def favicon():
|
|
280
|
+
return FileResponse(Path(__file__).parent / "ngdist" / "ng_app" / "favicon.ico")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# Catch-all route. serve index.html.
|
|
284
|
+
@app.get("/{full_path:path}")
|
|
285
|
+
def index(full_path: Path):
|
|
286
|
+
return FileResponse(Path(__file__).parent / "ngdist" / "ng_app" / "index.html")
|
|
287
|
+
|
|
288
|
+
|
|
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
|
+
def run_app(host: str, port: int):
|
|
299
|
+
get_static_files_if_needed()
|
|
300
|
+
uvicorn.run(app, host=host, port=port)
|
|
@@ -444,8 +444,8 @@
|
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
}
|
|
447
|
-
</style><link rel="stylesheet" href="/static/styles-
|
|
447
|
+
</style><link rel="stylesheet" href="/static/styles-4V6RXJMC.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="/static/styles-4V6RXJMC.css"></noscript></head>
|
|
448
448
|
<body>
|
|
449
449
|
<app-root></app-root>
|
|
450
|
-
<script src="/static/main-
|
|
450
|
+
<script src="/static/main-LJHMLKBL.js" type="module"></script></body>
|
|
451
451
|
</html>
|