Skip to content

Commit fcb3fae

Browse files
committed
pass the path with replaced UUIDs?
1 parent 42f3e7a commit fcb3fae

File tree

1 file changed

+387
-0
lines changed

1 file changed

+387
-0
lines changed
Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
"""
2+
Models used in storage API:
3+
4+
Specifically services/storage/src/simcore_service_storage/api/v0/openapi.yaml#/components/schemas
5+
6+
IMPORTANT: DO NOT COUPLE these schemas until storage is refactored
7+
"""
8+
9+
from datetime import datetime
10+
from enum import Enum
11+
from pathlib import Path
12+
from typing import Annotated, Any, Literal, Self, TypeAlias
13+
from uuid import UUID
14+
15+
from models_library.projects import ProjectID
16+
from models_library.users import UserID
17+
from pydantic import (
18+
BaseModel,
19+
ByteSize,
20+
ConfigDict,
21+
Field,
22+
PositiveInt,
23+
RootModel,
24+
StringConstraints,
25+
field_validator,
26+
model_validator,
27+
)
28+
from pydantic.networks import AnyUrl
29+
30+
from .basic_regex import DATCORE_DATASET_NAME_RE, S3_BUCKET_NAME_RE
31+
from .basic_types import SHA256Str
32+
from .generics import ListModel
33+
from .projects_nodes_io import (
34+
LocationID,
35+
LocationName,
36+
NodeID,
37+
SimcoreS3FileID,
38+
StorageFileID,
39+
)
40+
41+
ETag: TypeAlias = str
42+
43+
S3BucketName: TypeAlias = Annotated[str, StringConstraints(pattern=S3_BUCKET_NAME_RE)]
44+
45+
DatCoreDatasetName: TypeAlias = Annotated[
46+
str, StringConstraints(pattern=DATCORE_DATASET_NAME_RE)
47+
]
48+
49+
50+
# /
51+
class HealthCheck(BaseModel):
52+
name: str | None
53+
status: str | None
54+
api_version: str | None
55+
version: str | None
56+
57+
58+
# /locations
59+
class FileLocation(BaseModel):
60+
name: LocationName
61+
id: LocationID
62+
63+
model_config = ConfigDict(
64+
extra="forbid",
65+
json_schema_extra={
66+
"examples": [
67+
{"name": "simcore.s3", "id": 0},
68+
{"name": "datcore", "id": 1},
69+
]
70+
},
71+
)
72+
73+
74+
FileLocationArray: TypeAlias = ListModel[FileLocation]
75+
76+
77+
# /locations/{location_id}/datasets
78+
class DatasetMetaDataGet(BaseModel):
79+
dataset_id: UUID | DatCoreDatasetName
80+
display_name: str
81+
model_config = ConfigDict(
82+
extra="forbid",
83+
from_attributes=True,
84+
json_schema_extra={
85+
"examples": [
86+
# simcore dataset
87+
{
88+
"dataset_id": "74a84992-8c99-47de-b88a-311c068055ea",
89+
"display_name": "api",
90+
},
91+
{
92+
"dataset_id": "1c46752c-b096-11ea-a3c4-02420a00392e",
93+
"display_name": "Octave JupyterLab",
94+
},
95+
{
96+
"dataset_id": "2de04d1a-f346-11ea-9c22-02420a00085a",
97+
"display_name": "Sleepers",
98+
},
99+
# datcore datasets
100+
{
101+
"dataset_id": "N:dataset:be862eb8-861e-4b36-afc3-997329dd02bf",
102+
"display_name": "simcore-testing-bucket",
103+
},
104+
{
105+
"dataset_id": "N:dataset:9ad8adb0-8ea2-4be6-bc45-ecbec7546393",
106+
"display_name": "YetAnotherTest",
107+
},
108+
]
109+
},
110+
)
111+
112+
113+
UNDEFINED_SIZE_TYPE: TypeAlias = Literal[-1]
114+
UNDEFINED_SIZE: UNDEFINED_SIZE_TYPE = -1
115+
116+
117+
class FileMetaDataGetv010(BaseModel):
118+
file_uuid: str
119+
location_id: LocationID
120+
location: LocationName
121+
bucket_name: str
122+
object_name: str
123+
project_id: ProjectID | None
124+
project_name: str | None
125+
node_id: NodeID | None
126+
node_name: str | None
127+
file_name: str
128+
user_id: UserID | None
129+
user_name: str | None
130+
131+
model_config = ConfigDict(extra="forbid", frozen=True)
132+
133+
134+
class FileMetaDataGet(BaseModel):
135+
# Used by frontend
136+
file_uuid: str = Field(
137+
description="NOT a unique ID, like (api|uuid)/uuid/file_name or DATCORE folder structure",
138+
)
139+
location_id: LocationID = Field(..., description="Storage location")
140+
project_name: str | None = Field(
141+
default=None,
142+
description="optional project name, used by frontend to display path",
143+
)
144+
node_name: str | None = Field(
145+
default=None,
146+
description="optional node name, used by frontend to display path",
147+
)
148+
file_name: str = Field(..., description="Display name for a file")
149+
file_id: StorageFileID = Field(
150+
...,
151+
description="THIS IS the unique ID for the file. either (api|project_id)/node_id/file_name.ext for S3 and N:package:UUID for datcore",
152+
)
153+
created_at: datetime
154+
last_modified: datetime
155+
file_size: UNDEFINED_SIZE_TYPE | ByteSize = Field(
156+
default=UNDEFINED_SIZE, description="File size in bytes (-1 means invalid)"
157+
)
158+
entity_tag: ETag | None = Field(
159+
default=None,
160+
description="Entity tag (or ETag), represents a specific version of the file, None if invalid upload or datcore",
161+
)
162+
is_soft_link: bool = Field(
163+
default=False,
164+
description="If true, this file is a soft link."
165+
"i.e. is another entry with the same object_name",
166+
)
167+
is_directory: bool = Field(default=False, description="if True this is a directory")
168+
sha256_checksum: SHA256Str | None = Field(
169+
default=None,
170+
description="SHA256 message digest of the file content. Main purpose: cheap lookup.",
171+
)
172+
173+
model_config = ConfigDict(
174+
extra="ignore",
175+
from_attributes=True,
176+
json_schema_extra={
177+
"examples": [
178+
# typical S3 entry
179+
{
180+
"created_at": "2020-06-17 12:28:55.705340",
181+
"entity_tag": "8711cf258714b2de5498f5a5ef48cc7b",
182+
"file_id": "1c46752c-b096-11ea-a3c4-02420a00392e/e603724d-4af1-52a1-b866-0d4b792f8c4a/work.zip",
183+
"file_name": "work.zip",
184+
"file_size": 17866343,
185+
"file_uuid": "1c46752c-b096-11ea-a3c4-02420a00392e/e603724d-4af1-52a1-b866-0d4b792f8c4a/work.zip",
186+
"is_soft_link": False,
187+
"last_modified": "2020-06-22 13:48:13.398000+00:00",
188+
"location_id": 0,
189+
"node_name": "JupyterLab Octave",
190+
"project_name": "Octave JupyterLab",
191+
},
192+
# typical directory entry
193+
{
194+
"created_at": "2020-06-17 12:28:55.705340",
195+
"entity_tag": "8711cf258714b2de5498f5a5ef48cc7b",
196+
"file_id": "9a759caa-9890-4537-8c26-8edefb7a4d7c/be165f45-ddbf-4911-a04d-bc0b885914ef/workspace",
197+
"file_name": "workspace",
198+
"file_size": -1,
199+
"file_uuid": "9a759caa-9890-4537-8c26-8edefb7a4d7c/be165f45-ddbf-4911-a04d-bc0b885914ef/workspace",
200+
"is_soft_link": False,
201+
"last_modified": "2020-06-22 13:48:13.398000+00:00",
202+
"location_id": 0,
203+
"node_name": None,
204+
"project_name": None,
205+
"is_directory": True,
206+
},
207+
# api entry (not soft link)
208+
{
209+
"created_at": "2020-06-17 12:28:55.705340",
210+
"entity_tag": "8711cf258714b2de5498f5a5ef48cc7b",
211+
"file_id": "api/7b6b4e3d-39ae-3559-8765-4f815a49984e/tmpf_qatpzx",
212+
"file_name": "tmpf_qatpzx",
213+
"file_size": 86,
214+
"file_uuid": "api/7b6b4e3d-39ae-3559-8765-4f815a49984e/tmpf_qatpzx",
215+
"is_soft_link": False,
216+
"last_modified": "2020-06-22 13:48:13.398000+00:00",
217+
"location_id": 0,
218+
"node_name": None,
219+
"project_name": None,
220+
},
221+
# api entry (soft link)
222+
{
223+
"created_at": "2020-06-17 12:28:55.705340",
224+
"entity_tag": "36aa3644f526655a6f557207e4fd25b8",
225+
"file_id": "api/6f788ad9-0ad8-3d0d-9722-72f08c24a212/output_data.json",
226+
"file_name": "output_data.json",
227+
"file_size": 183,
228+
"file_uuid": "api/6f788ad9-0ad8-3d0d-9722-72f08c24a212/output_data.json",
229+
"is_soft_link": True,
230+
"last_modified": "2020-06-22 13:48:13.398000+00:00",
231+
"location_id": 0,
232+
"node_name": None,
233+
"project_name": None,
234+
},
235+
# datcore entry
236+
{
237+
"created_at": "2020-05-28T15:48:34.386302+00:00",
238+
"entity_tag": None,
239+
"file_id": "N:package:ce145b61-7e4f-470b-a113-033653e86d3d",
240+
"file_name": "templatetemplate.json",
241+
"file_size": 238,
242+
"file_uuid": "Kember Cardiac Nerve Model/templatetemplate.json",
243+
"is_soft_link": False,
244+
"last_modified": "2020-05-28T15:48:37.507387+00:00",
245+
"location_id": 1,
246+
"node_name": None,
247+
"project_name": None,
248+
},
249+
]
250+
},
251+
)
252+
253+
@field_validator("location_id", mode="before")
254+
@classmethod
255+
def ensure_location_is_integer(cls, v):
256+
if v is not None:
257+
return int(v)
258+
return v
259+
260+
261+
class FileMetaDataArray(RootModel[list[FileMetaDataGet]]):
262+
root: list[FileMetaDataGet] = Field(default_factory=list)
263+
264+
265+
# /locations/{location_id}/files/{file_id}
266+
267+
268+
class LinkType(str, Enum):
269+
PRESIGNED = "PRESIGNED"
270+
S3 = "S3"
271+
272+
273+
class PresignedLink(BaseModel):
274+
link: AnyUrl
275+
276+
277+
class FileUploadLinks(BaseModel):
278+
abort_upload: AnyUrl
279+
complete_upload: AnyUrl
280+
281+
282+
class FileUploadSchema(BaseModel):
283+
chunk_size: ByteSize
284+
urls: list[AnyUrl]
285+
links: FileUploadLinks
286+
287+
model_config = ConfigDict(
288+
extra="forbid",
289+
json_schema_extra={
290+
"examples": [
291+
# typical S3 entry
292+
{
293+
"chunk_size": "10000000",
294+
"urls": [
295+
"https://s3.amazonaws.com/bucket-name/key-name?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Expires=1698298164&Signature=WObYM%2F%2B4t7O3%2FZS3Kegb%2Bc4%3D",
296+
],
297+
"links": {
298+
"abort_upload": "https://storage.com:3021/bucket-name/key-name:abort",
299+
"complete_upload": "https://storage.com:3021/bucket-name/key-name:complete",
300+
},
301+
},
302+
]
303+
},
304+
)
305+
306+
307+
class TableSynchronisation(BaseModel):
308+
dry_run: bool | None = None
309+
fire_and_forget: bool | None = None
310+
removed: list[str]
311+
312+
313+
# /locations/{location_id}/files/{file_id}:complete
314+
class UploadedPart(BaseModel):
315+
number: PositiveInt
316+
e_tag: ETag
317+
318+
319+
class FileUploadCompletionBody(BaseModel):
320+
parts: list[UploadedPart]
321+
322+
@field_validator("parts")
323+
@classmethod
324+
def ensure_sorted(cls, value: list[UploadedPart]) -> list[UploadedPart]:
325+
return sorted(value, key=lambda uploaded_part: uploaded_part.number)
326+
327+
328+
class FileUploadCompleteLinks(BaseModel):
329+
state: AnyUrl
330+
331+
332+
class FileUploadCompleteResponse(BaseModel):
333+
links: FileUploadCompleteLinks
334+
335+
336+
# /locations/{location_id}/files/{file_id}:complete/futures/{future_id}
337+
class FileUploadCompleteState(Enum):
338+
OK = "ok"
339+
NOK = "nok"
340+
341+
342+
class FileUploadCompleteFutureResponse(BaseModel):
343+
state: FileUploadCompleteState
344+
e_tag: ETag | None = Field(default=None)
345+
346+
347+
# /simcore-s3/
348+
349+
350+
class FoldersBody(BaseModel):
351+
source: Annotated[dict[str, Any], Field(default_factory=dict)]
352+
destination: Annotated[dict[str, Any], Field(default_factory=dict)]
353+
nodes_map: Annotated[dict[NodeID, NodeID], Field(default_factory=dict)]
354+
355+
@model_validator(mode="after")
356+
def ensure_consistent_entries(self: Self) -> Self:
357+
source_node_keys = (NodeID(n) for n in self.source.get("workbench", {}))
358+
if set(source_node_keys) != set(self.nodes_map.keys()):
359+
msg = "source project nodes do not fit with nodes_map entries"
360+
raise ValueError(msg)
361+
destination_node_keys = (
362+
NodeID(n) for n in self.destination.get("workbench", {})
363+
)
364+
if set(destination_node_keys) != set(self.nodes_map.values()):
365+
msg = "destination project nodes do not fit with nodes_map values"
366+
raise ValueError(msg)
367+
return self
368+
369+
370+
class SoftCopyBody(BaseModel):
371+
link_id: SimcoreS3FileID
372+
373+
374+
class PathMetaDataGet(BaseModel):
375+
raw_path: Annotated[
376+
Path, Field(description="the partial s3 object key containing UUIDs")
377+
]
378+
path: Annotated[
379+
Path,
380+
Field(
381+
description="the partial s3 object with UUIDs replaced by names (such as project name, node name instead of UUIDs)"
382+
),
383+
]
384+
created_at: datetime
385+
last_modified: datetime
386+
387+
file_meta_data: FileMetaDataGet | None

0 commit comments

Comments
 (0)