divbase-lib 0.1.0a1__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.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0a1"
File without changes
@@ -0,0 +1,18 @@
1
+ """
2
+ Schemas for announcements.
3
+ """
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class AnnouncementResponse(BaseModel):
9
+ """Response model for an announcement returned by the API."""
10
+
11
+ heading: str = Field(..., description="Title of the announcement.")
12
+ message: str | None = Field(None, description="Detailed message of the announcement.")
13
+ level: str = Field(
14
+ ...,
15
+ description="The announcement level, which can control styling of announcement. Possible values are: info, success, warning, danger.",
16
+ )
17
+
18
+ model_config = ConfigDict(from_attributes=True)
@@ -0,0 +1,34 @@
1
+ """
2
+ Schemas for login + access and refresh tokens
3
+ """
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class CLILoginResponse(BaseModel):
9
+ """Response model for API (aka divbase-cli) login endpoint."""
10
+
11
+ access_token: str = Field(..., description="Bearer access token for authentication")
12
+ access_token_expires_at: int = Field(..., description="Unix timestamp when the access token expires")
13
+ refresh_token: str = Field(..., description="Bearer refresh token for obtaining new access tokens")
14
+ refresh_token_expires_at: int = Field(..., description="Unix timestamp when the refresh token expires")
15
+ email: str = Field(..., description="Email of the authenticated user")
16
+
17
+
18
+ class RefreshTokenRequest(BaseModel):
19
+ """Request model for refresh token endpoint."""
20
+
21
+ refresh_token: str = Field(..., description="Bearer refresh token for obtaining a new access token")
22
+
23
+
24
+ class RefreshTokenResponse(BaseModel):
25
+ """Response model for refresh token endpoint."""
26
+
27
+ access_token: str = Field(..., description="Bearer access token for authentication")
28
+ expires_at: int = Field(..., description="Unix timestamp when the access token expires")
29
+
30
+
31
+ class LogoutRequest(BaseModel):
32
+ """Request model for logout endpoint."""
33
+
34
+ refresh_token: str = Field(..., description="Bearer refresh token to be revoked on logout")
@@ -0,0 +1,42 @@
1
+ """
2
+ Schemas for personal access token related endpoints.
3
+ """
4
+
5
+ from pydantic import BaseModel, Field, model_validator
6
+ from typing_extensions import Self
7
+
8
+ # This needs to stay in sync with ProjectRoles in divbase-api.
9
+ # Redefined here to avoid a circular dependency on divbase-api.
10
+ ALLOWED_PROJECT_ROLES = ["read", "edit", "manage"]
11
+
12
+
13
+ class PATPermissions(BaseModel):
14
+ """
15
+ Personal Access Token permissions model.
16
+ This is stored in the db as JSONB in the PersonalAccessTokenDB.permissions.
17
+
18
+ - all_projects: True = access all the user's projects with same role as user
19
+ False = access only the projects listed in `projects`.
20
+ - projects: {str(project_id): role_string}; only enforced when all_projects=False.
21
+ Where role_string must be a valid ProjectRoles enum value.
22
+ - task_history: endpoint level scope.
23
+ For the 2 task history routes not tied to a specific project.
24
+ (these are 'task-history id' and for 'task-history user')
25
+ """
26
+
27
+ all_projects: bool = False
28
+ projects: dict[str, str] = Field(default_factory=dict)
29
+ task_history: bool = False
30
+
31
+ @model_validator(mode="after")
32
+ def no_project_scope_if_all_projects_true(self) -> Self:
33
+ if self.all_projects and self.projects:
34
+ raise ValueError("cannot specify project scope if 'all_projects' is True")
35
+ return self
36
+
37
+ @model_validator(mode="after")
38
+ def validate_project_roles(self) -> Self:
39
+ for project_id, role_str in self.projects.items():
40
+ if role_str not in ALLOWED_PROJECT_ROLES:
41
+ raise ValueError(f"invalid role '{role_str}' for project {project_id}")
42
+ return self
@@ -0,0 +1,87 @@
1
+ """
2
+ Schemas for project versioning routes.
3
+
4
+ Project versions are the state of all files in a project's storage bucket at a given time point.
5
+ """
6
+
7
+ from typing import TypedDict
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ # Request Models
13
+ class CreateVersioningFileRequest(BaseModel):
14
+ name: str = Field(..., description="Initial version name")
15
+ description: str = Field(..., description="Initial version description")
16
+
17
+
18
+ class AddVersionRequest(BaseModel):
19
+ name: str = Field(..., description="Name of the new version to add")
20
+ description: str = Field("", description="Description of the new version")
21
+
22
+
23
+ class DeleteVersionRequest(BaseModel):
24
+ version_name: str = Field(..., description="Name of the version to delete")
25
+
26
+
27
+ class UpdateVersionRequest(BaseModel):
28
+ version_name: str = Field(..., description="Name of the version to update")
29
+ new_name: str | None = Field(None, description="New name for the version")
30
+ new_description: str | None = Field(None, description="New description for the version")
31
+
32
+
33
+ # Response Models
34
+ class ProjectBasicInfo(BaseModel):
35
+ """Base model for describing a single project version, not for direct use in an endpoint."""
36
+
37
+ name: str = Field(..., description="Version name")
38
+ description: str | None = Field(..., description="Version description")
39
+
40
+
41
+ class AddVersionResponse(ProjectBasicInfo):
42
+ """Response model for adding a version."""
43
+
44
+ created_at: str = Field(..., description="ISO timestamp when version was created")
45
+
46
+
47
+ class ProjectVersionInfo(ProjectBasicInfo):
48
+ """Basic information about a project version. You get a list of these when listing all versions in a project."""
49
+
50
+ created_at: str = Field(..., description="ISO timestamp when version was created")
51
+ is_deleted: bool = Field(..., description="Whether this version has been soft-deleted")
52
+
53
+
54
+ # Typed dict used as this is used in the db model to define the contents of the JSONB
55
+ class FileDetails(TypedDict):
56
+ """Details about a single file in a project version, ETag (is checksum) and size is in bytes."""
57
+
58
+ version_id: str
59
+ etag: str
60
+ size: int
61
+
62
+
63
+ class ProjectVersionDetailResponse(ProjectBasicInfo):
64
+ """Full information about a single project version, including the files at that version."""
65
+
66
+ created_at: str = Field(..., description="ISO timestamp when version was created")
67
+ is_deleted: bool = Field(..., description="Whether this version has been soft-deleted")
68
+ files: dict[str, FileDetails] = Field(
69
+ ..., description="Info about all files at this version, including: version IDs, etags and file sizes"
70
+ )
71
+
72
+
73
+ class DeleteVersionResponse(BaseModel):
74
+ """Response model for deleting a version."""
75
+
76
+ name: str = Field(..., description="Name of the version that was deleted")
77
+ already_deleted: bool = Field(
78
+ False,
79
+ description="Whether the version was already soft-deleted before this request",
80
+ )
81
+ date_deleted: str = Field(..., description="ISO timestamp of when the version was soft-deleted")
82
+
83
+
84
+ class UpdateVersionResponse(ProjectBasicInfo):
85
+ """Response model for updating a version."""
86
+
87
+ created_at: str = Field(..., description="ISO timestamp when version was created")
@@ -0,0 +1,209 @@
1
+ """
2
+ Schemas for query routes.
3
+ """
4
+
5
+ from typing import Optional, Self
6
+
7
+ from pydantic import BaseModel, ConfigDict, field_validator, model_validator
8
+
9
+ from divbase_lib.utils import split_semicolon_bcftools_command_segments
10
+
11
+ # Shared helper functions and models
12
+
13
+
14
+ def validate_command_not_empty(command: str) -> str:
15
+ """
16
+ Validator function to ensure that the user-submitted bcftools command string is not empty or just whitespace.
17
+ If there are multiple segments separated by semicolons, it also validates that none of the segments are empty or whitespace.
18
+
19
+ Shared helper for command field validators in BcftoolsQueryRequest.command and BcftoolsQueryKwargs.command.
20
+ """
21
+ command_string = command.strip()
22
+ if command_string == "":
23
+ raise ValueError(
24
+ "The --command option must be a non-empty bcftools view string. "
25
+ 'If you only want to subset based on samples, use --command "view -s" in combination with one of the sample-selection options: --tsv-filter, --samples, --samples-file.'
26
+ )
27
+
28
+ segments = split_semicolon_bcftools_command_segments(command_string)
29
+ for position, segment in enumerate(segments, start=1):
30
+ if segment.strip() == "":
31
+ raise ValueError(
32
+ f"The --command option has an empty pipeline segment at position {position}. "
33
+ "Use semicolons only between complete bcftools view commands."
34
+ )
35
+
36
+ return command
37
+
38
+
39
+ class SharedBaseModel(BaseModel):
40
+ """Shared pydantic BaseModel for VCF query response and kwarg schemas."""
41
+
42
+ model_config = ConfigDict(
43
+ validate_assignment=True,
44
+ json_schema_extra={
45
+ "description": ("Exactly one sample-selection mode must be provided: tsv_filter, samples, or all_samples.")
46
+ },
47
+ )
48
+
49
+
50
+ # Request models
51
+ class SampleMetadataQueryRequest(BaseModel):
52
+ """Request model for sample metadata query route."""
53
+
54
+ tsv_filter: str
55
+ metadata_tsv_name: str
56
+
57
+
58
+ class BcftoolsQueryRequest(SharedBaseModel):
59
+ """
60
+ Request model for sample metadata query route.
61
+ See ConfigDict in SharedBaseModel for validation rules around tsv_filter and samples fields.
62
+ """
63
+
64
+ tsv_filter: str | None = None # Used for metadata mode only
65
+ metadata_tsv_name: str | None = None # Used for metadata mode only
66
+ command: str # bcftools command input with --command
67
+ samples: list[str] | None = None
68
+ all_samples: bool = False
69
+
70
+ @field_validator("command")
71
+ @classmethod
72
+ def validate_command(_cls, value: str) -> str:
73
+ return validate_command_not_empty(value)
74
+
75
+ @model_validator(mode="after")
76
+ def validate_sample_selection_mode(self) -> Self:
77
+ tsv_filter = self.tsv_filter
78
+ samples = self.samples
79
+ all_samples = self.all_samples
80
+
81
+ selection_count = 0
82
+ if tsv_filter is not None:
83
+ selection_count += 1
84
+ if samples is not None:
85
+ selection_count += 1
86
+ if all_samples:
87
+ selection_count += 1
88
+
89
+ if selection_count > 1:
90
+ raise ValueError("Only one of tsv_filter, samples, or all_samples may be provided.")
91
+ if selection_count == 0:
92
+ raise ValueError("One sample-selection mode must be provided (tsv_filter, samples, or all_samples).")
93
+
94
+ if tsv_filter is not None and self.metadata_tsv_name is None:
95
+ raise ValueError("metadata_tsv_name must be provided when tsv_filter is used.")
96
+
97
+ if samples is not None and len(samples) == 0:
98
+ raise ValueError("samples must contain at least one sample ID when provided.")
99
+
100
+ return self
101
+
102
+
103
+ # Models for task kwargs and task results. Reused in task history schemas too, hence pydantic models and not just dataclasses.
104
+ class SampleMetadataQueryKwargs(BaseModel):
105
+ """Keyword arguments for sample metadata query task. Used to pass info to Celery task, and also for recording task history."""
106
+
107
+ tsv_filter: str
108
+ metadata_tsv_name: str
109
+ bucket_name: str
110
+ project_id: int
111
+ project_name: str
112
+ user_id: int
113
+
114
+
115
+ class BcftoolsQueryKwargs(SharedBaseModel):
116
+ """
117
+ Keyword arguments for BCFtools query task. Used to pass info to Celery task, and also for recording task history.
118
+ See ConfigDict in SharedBaseModel for validation rules around tsv_filter and samples fields.
119
+ """
120
+
121
+ tsv_filter: str | None = None # Used for metadata mode only
122
+ metadata_tsv_name: str | None = None # Used for metadata mode only
123
+ command: str # bcftools command input with --command
124
+ bucket_name: str
125
+ project_id: int
126
+ project_name: str
127
+ user_id: int
128
+ job_id: int
129
+ samples: list[str] | None = None
130
+ all_samples: bool = False
131
+
132
+ @field_validator("command")
133
+ @classmethod
134
+ def validate_command(_cls, value: str) -> str:
135
+ return validate_command_not_empty(value)
136
+
137
+ @model_validator(mode="after")
138
+ def validate_sample_selection_mode(self) -> Self:
139
+ tsv_filter = self.tsv_filter
140
+ samples = self.samples
141
+ all_samples = self.all_samples
142
+
143
+ selection_count = 0
144
+ if tsv_filter is not None:
145
+ selection_count += 1
146
+ if samples is not None:
147
+ selection_count += 1
148
+ if all_samples:
149
+ selection_count += 1
150
+
151
+ if selection_count > 1:
152
+ raise ValueError("Only one of tsv_filter, samples, or all_samples may be provided.")
153
+ if selection_count == 0:
154
+ # Backward compatibility for historical task kwargs created before all_samples option was implemented.
155
+ # To ensure that task-history deserialization does not break for existing tasks.
156
+ # Could be handled by a backfilling migration instead.
157
+ self.all_samples = True
158
+ return self
159
+
160
+ if tsv_filter is not None and self.metadata_tsv_name is None:
161
+ raise ValueError("metadata_tsv_name must be provided when tsv_filter is used.")
162
+
163
+ if samples is not None and len(samples) == 0:
164
+ raise ValueError("samples must contain at least one sample ID when provided.")
165
+
166
+ return self
167
+
168
+
169
+ class SampleFileMappingResult(BaseModel):
170
+ sample_id: str
171
+ filename: str
172
+
173
+ @model_validator(mode="before")
174
+ @classmethod
175
+ def map_legacy_task_history_keys(cls, data):
176
+ """
177
+ Support historical task-history payloads that stored uppercase keys.
178
+ """
179
+ if not isinstance(data, dict):
180
+ return data
181
+
182
+ normalized = dict(data)
183
+ if "sample_id" not in normalized:
184
+ if "Sample_ID" in normalized:
185
+ normalized["sample_id"] = normalized["Sample_ID"]
186
+ elif "#Sample_ID" in normalized:
187
+ normalized["sample_id"] = normalized["#Sample_ID"]
188
+ if "filename" not in normalized and "Filename" in normalized:
189
+ normalized["filename"] = normalized["Filename"]
190
+
191
+ return normalized
192
+
193
+
194
+ class SampleMetadataQueryTaskResult(BaseModel):
195
+ """Metadata query task result details. Based on the return of tasks.sample_metadata_query."""
196
+
197
+ sample_and_filename_subset: list[SampleFileMappingResult]
198
+ unique_sample_ids: list[str]
199
+ unique_filenames: list[str]
200
+ query_message: str
201
+ warnings: list[str] = []
202
+ status: str | None = None
203
+
204
+
205
+ class BcftoolsQueryTaskResult(BaseModel):
206
+ """BCFtools query task result details. Based on the return of tasks.bcftools_query."""
207
+
208
+ output_file: str
209
+ status: Optional[str] = None
@@ -0,0 +1,214 @@
1
+ """
2
+ Schemas for DivBase's S3 API routes.
3
+
4
+ Pre-signed download URLs do not need to account for single vs multipart as this can be controlled by the client
5
+ using the HTTP range header when downloading (so you only need 1 pre-signed URL per object for download).
6
+
7
+ Pre-signed upload URLs need to account for single vs multipart uploads hence all the extra schemas below.
8
+ """
9
+
10
+ from datetime import datetime
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+ from divbase_lib.divbase_constants import S3_MULTIPART_CHUNK_SIZE
15
+
16
+ MB = 1024 * 1024
17
+
18
+
19
+ ## list objects models ##
20
+ class ListObjectsRequest(BaseModel):
21
+ """Request model for listing objects in an S3 bucket."""
22
+
23
+ prefix: str | None = Field(None, description="Optional prefix to filter objects by name.")
24
+ next_token: str | None = Field(
25
+ None, description="Token to continue listing files from the end of a previous request."
26
+ )
27
+
28
+
29
+ class ObjectDetails(BaseModel):
30
+ """Details about a single object in an S3 bucket."""
31
+
32
+ name: str = Field(..., description="The name of the object in the bucket.")
33
+ size: int = Field(..., description="The size of the object in bytes.")
34
+ last_modified: datetime = Field(..., description="The date and time the object was last modified.")
35
+ etag: str = Field(..., description="The ETag of the object, which is the MD5 checksum.")
36
+
37
+
38
+ class ListObjectsResponse(BaseModel):
39
+ """Response model for listing objects in an S3 bucket."""
40
+
41
+ objects: list[ObjectDetails] = Field(
42
+ ..., description="A list of objects in the bucket.", min_length=0, max_length=1000
43
+ )
44
+ next_token: str | None = Field(
45
+ None, description="Token for fetching the next page of results. If None, no more results."
46
+ )
47
+
48
+
49
+ ## list soft-deleted objects models ##
50
+ class SoftDeletedObjectDetails(BaseModel):
51
+ """Details about a single soft-deleted object in an S3 bucket."""
52
+
53
+ name: str = Field(..., description="The name of the object in the bucket.")
54
+ last_modified: datetime = Field(..., description="The date and time the object was deleted.")
55
+
56
+
57
+ ## file info models ##
58
+ class ObjectVersionInfo(BaseModel):
59
+ """Detailed information about a single version of an S3 object."""
60
+
61
+ version_id: str = Field(..., description="The version ID of the object.")
62
+ last_modified: datetime = Field(..., description="The date and time the object version was last modified.")
63
+ size: int = Field(..., description="The size of the object in bytes.")
64
+ etag: str = Field(..., description="The ETag of the object, which is the MD5 checksum.")
65
+ is_latest: bool = Field(..., description="Indicates if this is the latest version of the object.")
66
+
67
+
68
+ class ObjectInfoResponse(BaseModel):
69
+ """Response model for detailed information about all versions of a single object stored in S3."""
70
+
71
+ object_name: str = Field(..., description="The name of the object.")
72
+ is_currently_deleted: bool = Field(..., description="True if the latest version of the object is a delete marker.")
73
+ versions: list[ObjectVersionInfo] = Field(..., description="A list of all versions of the object.")
74
+
75
+
76
+ ## download models ##
77
+ class DownloadObjectRequest(BaseModel):
78
+ """Request model to download a single object using a pre-signed URL."""
79
+
80
+ name: str = Field(..., description="Name of the object to be downloaded")
81
+ version_id: str | None = Field(..., description="Version ID of the object, None if latest version")
82
+
83
+
84
+ class PreSignedDownloadResponse(BaseModel):
85
+ """Response model to download a single object using the pre-signed URL and (optionally) version ID."""
86
+
87
+ name: str = Field(..., description="Name of the object to be downloaded")
88
+ pre_signed_url: str = Field(..., description="Pre-signed URL for downloading the object")
89
+ version_id: str | None = Field(..., description="Version ID of the object, None if latest version")
90
+
91
+
92
+ ### Single-part upload models ###
93
+ class UploadSinglePartObjectRequest(BaseModel):
94
+ """Request model to upload a single object as a single part using a pre-signed URL."""
95
+
96
+ name: str = Field(..., description="Name of the object to be uploaded")
97
+ content_length: int = Field(..., description="Size of the file in bytes")
98
+ md5_hash: str | None = Field(None, description="Optional MD5 hash of the object for integrity check")
99
+
100
+
101
+ class PreSignedSinglePartUploadResponse(BaseModel):
102
+ """Response model to upload a single object as a single part using the pre-signed URL using PUT."""
103
+
104
+ name: str = Field(..., description="Name of the object to be uploaded")
105
+ pre_signed_url: str = Field(..., description="Pre-signed URL to which the file should be uploaded")
106
+ put_headers: dict[str, str] = Field(..., description="Headers to be included in the PUT request")
107
+
108
+
109
+ ### Multipart upload models ###
110
+ class CreateMultipartUploadRequest(BaseModel):
111
+ """Request model to create a multipart upload using pre-signed URLs."""
112
+
113
+ name: str = Field(..., description="Name of the object to be uploaded")
114
+ content_length: int = Field(..., description="Size of the file in bytes")
115
+
116
+
117
+ class CreateMultipartUploadResponse(BaseModel):
118
+ """Response model to create a multipart upload using pre-signed URLs."""
119
+
120
+ name: str = Field(..., description="Name of the object to be uploaded")
121
+ upload_id: str = Field(..., description="Upload ID for the multipart upload")
122
+ number_of_parts: int = Field(..., description="Total number of parts required for the upload", ge=1, le=10000)
123
+ part_size: int = Field(
124
+ S3_MULTIPART_CHUNK_SIZE, description="Size of each part in bytes (the last part may be smaller)."
125
+ )
126
+
127
+
128
+ class GetPresignedPartUrlsRequest(BaseModel):
129
+ """
130
+ Request model to get pre-signed URLs for multiple parts of a presigned multipart upload.
131
+
132
+ You can request up to 100 parts at a time.
133
+ Part number indexing is 1-based (with max allowed range: 1 to 10000).
134
+ """
135
+
136
+ name: str = Field(..., description="Name of the object to be uploaded")
137
+ upload_id: str = Field(..., description="Upload ID for the multipart upload")
138
+ parts_range_start: int = Field(..., description="Starting part number", ge=1, le=10000)
139
+ parts_range_end: int = Field(..., description="Ending part number", ge=1, le=10000)
140
+ md5_checksums: list[str] | None = Field(
141
+ None, description="Optional list of MD5 checksums for each part to be uploaded"
142
+ )
143
+
144
+
145
+ class PresignedUploadPartUrlResponse(BaseModel):
146
+ """Response model for a pre-signed URL for a single part of a multipart upload."""
147
+
148
+ part_number: int = Field(..., description="Part number", ge=1, le=10000)
149
+ pre_signed_url: str = Field(..., description="Pre-signed URL for uploading this part")
150
+ headers: dict[str, str] = Field(..., description="Headers to be included in the PUT request for this part")
151
+
152
+
153
+ class UploadedPart(BaseModel):
154
+ """Model representing a part of an object that has been uploaded via multi-part upload."""
155
+
156
+ part_number: int = Field(..., description="Part number", ge=1, le=10000)
157
+ etag: str = Field(description="ETag returned by S3 after uploading the part")
158
+
159
+
160
+ class CompleteMultipartUploadRequest(BaseModel):
161
+ """Request model to complete a multipart upload using pre-signed URLs."""
162
+
163
+ name: str = Field(..., description="Name of the object to be uploaded")
164
+ upload_id: str = Field(..., description="Upload ID for the multipart upload")
165
+ parts: list[UploadedPart] = Field(..., description="List of parts that have been uploaded")
166
+
167
+
168
+ class CompleteMultipartUploadResponse(BaseModel):
169
+ """Response model to complete a multipart upload using pre-signed URLs."""
170
+
171
+ name: str = Field(..., description="Name of the object that was uploaded")
172
+ version_id: str = Field(..., description="Version ID of the uploaded object")
173
+ md5_hash: str = Field(..., description="MD5 hash of the uploaded object")
174
+
175
+
176
+ class AbortMultipartUploadRequest(BaseModel):
177
+ """Request model to abort a multipart upload and clean up parts."""
178
+
179
+ name: str = Field(..., description="Name of the object being uploaded")
180
+ upload_id: str = Field(..., description="Upload ID for the multipart upload to be aborted")
181
+
182
+
183
+ class AbortMultipartUploadResponse(BaseModel):
184
+ """Response model to abort a multipart upload."""
185
+
186
+ name: str = Field(..., description="Name of the object being uploaded")
187
+ upload_id: str = Field(..., description="Upload ID for the multipart upload that was aborted")
188
+
189
+
190
+ class RestoreObjectsResponse(BaseModel):
191
+ """Response model for restoring soft-deleted objects in a bucket."""
192
+
193
+ restored: list[str] = Field(
194
+ ...,
195
+ description="List of object names that were successfully restored, this includes objects that were already live",
196
+ )
197
+ not_restored: list[str] = Field(
198
+ ...,
199
+ description=(
200
+ "List of object names that could not be processed.\n"
201
+ "This could be due to several reasons:\n"
202
+ "1. The object does not exist in the bucket (e.g., a typo in the name).\n"
203
+ "2. The object was hard-deleted and is unrecoverable.\n"
204
+ "3. An unexpected server error occurred during the restore attempt."
205
+ ),
206
+ )
207
+
208
+
209
+ ## checksum models ##
210
+ class FileChecksumResponse(BaseModel):
211
+ """Response model for reporting a file's checksum in the bucket."""
212
+
213
+ object_name: str = Field(..., description="Name of the object in the bucket")
214
+ md5_checksum: str = Field(..., description="MD5 checksum of the object in the bucket")
@@ -0,0 +1,50 @@
1
+ """
2
+ Schemas for task history routes.
3
+ """
4
+
5
+ from typing import Any, Optional, Union
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from divbase_lib.api_schemas.queries import (
10
+ BcftoolsQueryKwargs,
11
+ BcftoolsQueryTaskResult,
12
+ SampleMetadataQueryKwargs,
13
+ SampleMetadataQueryTaskResult,
14
+ )
15
+ from divbase_lib.api_schemas.vcf_dimensions import DimensionUpdateKwargs, DimensionUpdateTaskResult
16
+
17
+
18
+ class TaskHistoryResult(BaseModel):
19
+ """
20
+ Task details as returned by queries to the SQAlchemy+pg results backend.
21
+ """
22
+
23
+ id: int
24
+ submitter_email: Optional[str] = None
25
+ status: Optional[str] = None
26
+ result: Optional[
27
+ Union[
28
+ dict[
29
+ str, Any
30
+ ], # Note! This dict must come first here so that error results are preserved and not incorrectly inserted into the result models
31
+ SampleMetadataQueryTaskResult,
32
+ BcftoolsQueryTaskResult,
33
+ DimensionUpdateTaskResult,
34
+ ]
35
+ ] = None
36
+ date_done: Optional[str] = None
37
+ name: Optional[str] = None
38
+ args: Optional[str] = None
39
+ kwargs: Optional[
40
+ Union[
41
+ SampleMetadataQueryKwargs,
42
+ BcftoolsQueryKwargs,
43
+ DimensionUpdateKwargs,
44
+ ]
45
+ ] = None
46
+ worker: Optional[str] = None
47
+ created_at: Optional[str] = None
48
+ started_at: Optional[str] = None
49
+ completed_at: Optional[str] = None
50
+ runtime: Optional[float] = None