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.
- divbase_lib/__init__.py +1 -0
- divbase_lib/api_schemas/__init__.py +0 -0
- divbase_lib/api_schemas/announcements.py +18 -0
- divbase_lib/api_schemas/auth.py +34 -0
- divbase_lib/api_schemas/personal_access_tokens.py +42 -0
- divbase_lib/api_schemas/project_versions.py +87 -0
- divbase_lib/api_schemas/queries.py +209 -0
- divbase_lib/api_schemas/s3.py +214 -0
- divbase_lib/api_schemas/task_history.py +50 -0
- divbase_lib/api_schemas/vcf_dimensions.py +67 -0
- divbase_lib/divbase_constants.py +56 -0
- divbase_lib/exceptions.py +156 -0
- divbase_lib/metadata_validator.py +827 -0
- divbase_lib/s3_checksums.py +120 -0
- divbase_lib/utils.py +76 -0
- divbase_lib-0.1.0a1.dist-info/METADATA +38 -0
- divbase_lib-0.1.0a1.dist-info/RECORD +18 -0
- divbase_lib-0.1.0a1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schemas for VCF dimensions routes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DimensionUpdateKwargs(BaseModel):
|
|
11
|
+
"""Keyword arguments for dimension update task. Used to pass info to Celery task, and also for recording task history."""
|
|
12
|
+
|
|
13
|
+
bucket_name: str
|
|
14
|
+
project_id: int
|
|
15
|
+
project_name: str
|
|
16
|
+
user_id: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DimensionUpdateTaskResult(BaseModel):
|
|
20
|
+
"""Dimension update task result details. Based on the return of tasks.update_dimensions_index."""
|
|
21
|
+
|
|
22
|
+
status: Optional[str] = None
|
|
23
|
+
VCF_files_added: Optional[list[str]] = Field(
|
|
24
|
+
None, description="VCF files that were added to dimensions index by this job"
|
|
25
|
+
)
|
|
26
|
+
VCF_files_skipped: Optional[list[str]] = Field(
|
|
27
|
+
None, description="VCF files skipped by this job (previous DivBase-generated result VCFs)"
|
|
28
|
+
)
|
|
29
|
+
VCF_files_deleted: Optional[list[str]] = Field(
|
|
30
|
+
None, description="VCF files that have been deleted from the project and thus have been dropped from the index"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DimensionsShowResult(BaseModel):
|
|
35
|
+
"""Result model for showing VCF dimensions for a project."""
|
|
36
|
+
|
|
37
|
+
project_id: int
|
|
38
|
+
project_name: str
|
|
39
|
+
vcf_file_count: int
|
|
40
|
+
vcf_files: list[dict]
|
|
41
|
+
skipped_file_count: int
|
|
42
|
+
skipped_files: list[dict]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DimensionsSamplesResult(BaseModel):
|
|
46
|
+
"""Result model for showing unique samples across project VCFs."""
|
|
47
|
+
|
|
48
|
+
unique_samples: list[str] # Already sorted, by the CRUD function get_unique_samples_by_project_async()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DimensionsScaffoldsResult(BaseModel):
|
|
52
|
+
"""Result model for showing unique scaffolds across project VCFs."""
|
|
53
|
+
|
|
54
|
+
unique_scaffolds: list[str] # Already sorted, by the CRUD function get_unique_scaffolds_by_project_async()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DimensionsVCFFileEntry(BaseModel):
|
|
58
|
+
"""A single VCF file entry in the unique VCF files response."""
|
|
59
|
+
|
|
60
|
+
vcf_file_s3_key: str
|
|
61
|
+
s3_version_id: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DimensionsVCFFilesResult(BaseModel):
|
|
65
|
+
"""Result model for showing unique VCF file/version entries across project VCFs."""
|
|
66
|
+
|
|
67
|
+
unique_vcf_files: list[DimensionsVCFFileEntry]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants that both divbase-api and divbase-cli need to agree on.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
ONE_MiB = 1024 * 1024
|
|
6
|
+
|
|
7
|
+
# When you download a file that has been uploaded in parts, you have
|
|
8
|
+
# to know the part/chunk size used in order to correctly calculate the composite checksum
|
|
9
|
+
S3_MULTIPART_CHUNK_SIZE = 32 * ONE_MiB
|
|
10
|
+
|
|
11
|
+
# At what point you swap from single part to multipart upload to S3.
|
|
12
|
+
# If server and client used the same threshold then makes life easier
|
|
13
|
+
# when validating the checksums of files in s3 as single part and multipart uploads use different ETag formats.
|
|
14
|
+
# (No benefit in constraining the download threshold, so not done here)
|
|
15
|
+
S3_MULTIPART_UPLOAD_THRESHOLD = 96 * ONE_MiB
|
|
16
|
+
|
|
17
|
+
# Max number of items that can be processed in a single API call to divbase-api's S3 routes
|
|
18
|
+
# covers e.g pre-signed urls for upload/download, soft delete and checksum comparisons
|
|
19
|
+
# client has to batch requests if exceeding this limit
|
|
20
|
+
MAX_S3_API_BATCH_SIZE = 100
|
|
21
|
+
|
|
22
|
+
# How long the pre-signed URLs divbase-api creates are valid for
|
|
23
|
+
SINGLE_PART_UPLOAD_URL_EXPIRATION_SECONDS = 3600 # 1 hour
|
|
24
|
+
MULTI_PART_UPLOAD_URL_EXPIRATION_SECONDS = 36000 # 10 hours
|
|
25
|
+
DOWNLOAD_URL_EXPIRATION_SECONDS = 36000 # 10 hours
|
|
26
|
+
|
|
27
|
+
# (Not used anywhere, just making it explicit)
|
|
28
|
+
# This is limited by our fixing of the chunk size and S3's limit to the number of chunks allowed (10,000)
|
|
29
|
+
# 320 GiB if using 32 MiB chunks
|
|
30
|
+
LARGEST_FILE_UPLOADABLE_TO_DIVBASE_BYTES = 10_000 * S3_MULTIPART_CHUNK_SIZE
|
|
31
|
+
|
|
32
|
+
# File types that DivBase supports
|
|
33
|
+
# Whilst we can't realistically limit what file types a user actually uploads,
|
|
34
|
+
# this is here to say what we know should work in DivBase.
|
|
35
|
+
SUPPORTED_DIVBASE_FILE_TYPES = (".tsv", ".vcf.gz", ".csi", ".tbi")
|
|
36
|
+
|
|
37
|
+
# Characters that are not allowed in file names uploaded to DivBase
|
|
38
|
+
# This is to prevent issues when users try to filter/query files on DivBase using these characters
|
|
39
|
+
# or when downloading files (e.g. ":" is used to specify file versions when downloading files
|
|
40
|
+
UNSUPPORTED_CHARACTERS_IN_FILENAMES = (":", "*", "?", "<", ">", "|", "\\")
|
|
41
|
+
|
|
42
|
+
# This prefix is used for all *.vcf.gz results files from a query job/task.
|
|
43
|
+
# After the prefix comes the job id which is a rolling integer.
|
|
44
|
+
# E.g. format: result_of_job_<job-id>.vcf.gz , where <job-id> = 1 and is auto-incremented for every new job.
|
|
45
|
+
# NOTE: If you update this, you should also update the S3 lifecycle policies associated with this.
|
|
46
|
+
QUERY_RESULTS_FILE_PREFIX = "result_of_job_"
|
|
47
|
+
|
|
48
|
+
# CLI includes this in every request to the server,
|
|
49
|
+
# and server uses it to determine if it should:
|
|
50
|
+
# 1. Reject the request as the version is too outdated
|
|
51
|
+
# 2. Add an announcement about a new CLI version being available when the user logs in.
|
|
52
|
+
# 3. Do Nothing as the user's CLI version is up to date.
|
|
53
|
+
CLI_VERSION_HEADER_KEY = "X-CLI-Version"
|
|
54
|
+
|
|
55
|
+
# Personal Access Tokens (PATs) are used for passwordless auth with DivBase API
|
|
56
|
+
PAT_TOKEN_PREFIX = "divbase_pat_"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for DivBase packages.
|
|
3
|
+
|
|
4
|
+
These are raised by lover-level functions/methods which understand the context of the error.
|
|
5
|
+
|
|
6
|
+
Note: By adding the `__str__` method to each exception,
|
|
7
|
+
we ensure that when you manually raise a specific exception the error message looks good
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BcftoolsEnvironmentError(Exception):
|
|
14
|
+
"""Raised when there's an issue with the execution environment (Docker, etc.)."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, container_name: str):
|
|
17
|
+
self.container_name = container_name
|
|
18
|
+
error_message = (
|
|
19
|
+
f"No running container found with name {self.container_name}. Ensure the Docker image is available.\n"
|
|
20
|
+
)
|
|
21
|
+
super().__init__(error_message)
|
|
22
|
+
|
|
23
|
+
self.error_message = error_message
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return self.error_message
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BcftoolsCommandError(Exception):
|
|
30
|
+
"""Raised when a bcftools command fails to execute properly."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, command: str, error_details: Exception = None):
|
|
33
|
+
self.command = command
|
|
34
|
+
self.error_details = error_details
|
|
35
|
+
|
|
36
|
+
error_message = f"bcftools command failed: '{command}'"
|
|
37
|
+
if error_details:
|
|
38
|
+
error_message += f" with error details: {error_details}"
|
|
39
|
+
|
|
40
|
+
super().__init__(error_message)
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
if hasattr(self.error_details, "stderr") and self.error_details.stderr:
|
|
44
|
+
return f"bcftools command failed: '{self.command}' with error: {self.error_details.stderr}"
|
|
45
|
+
return super().__str__()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BcftoolsPipeEmptyCommandError(Exception):
|
|
49
|
+
"""Raised when an empty command is provided to the bcftools pipe."""
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
error_message = "Empty command provided. Please specify at least one valid bcftools command."
|
|
53
|
+
super().__init__(error_message)
|
|
54
|
+
self.error_message = error_message
|
|
55
|
+
|
|
56
|
+
def __str__(self):
|
|
57
|
+
return self.error_message
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BcftoolsPipeUnsupportedCommandError(Exception):
|
|
61
|
+
"""Raised when a bcftools command unsupported by the BcftoolsQueryManager class is provided."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, command: str, position: int, valid_commands: list[str]):
|
|
64
|
+
self.command = command
|
|
65
|
+
self.position = position
|
|
66
|
+
self.valid_commands = valid_commands
|
|
67
|
+
|
|
68
|
+
message = (
|
|
69
|
+
f"Unsupported bcftools command '{command}' at position {position}. "
|
|
70
|
+
f"Only the following commands are supported: {', '.join(valid_commands)}"
|
|
71
|
+
)
|
|
72
|
+
super().__init__(message)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SidecarNoDataLoadedError(Exception):
|
|
76
|
+
"""Raised when no data is loaded in SidecarQueryManager."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, file_path: Path, submethod: str, error_details: str | None = None):
|
|
79
|
+
self.file_path = file_path
|
|
80
|
+
self.error_details = error_details
|
|
81
|
+
|
|
82
|
+
error_message = f"No data loaded from file '{file_path}', as raised in submethod '{submethod}'."
|
|
83
|
+
if error_details:
|
|
84
|
+
error_message += f"More details about the error: {error_details}"
|
|
85
|
+
super().__init__(error_message)
|
|
86
|
+
self.error_message = error_message
|
|
87
|
+
|
|
88
|
+
def __str__(self):
|
|
89
|
+
return self.error_message
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SidecarInvalidFilterError(Exception):
|
|
93
|
+
"""Raised when an invalid filter is provided to SidecarQueryManager."""
|
|
94
|
+
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SidecarColumnNotFoundError(Exception):
|
|
99
|
+
"""Raised when a requested column is not found in the query result."""
|
|
100
|
+
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SidecarSampleIDError(Exception):
|
|
105
|
+
"""Raised when a Sample_ID is invalid or duplicated when loading sidecar data."""
|
|
106
|
+
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class SidecarMetadataFormatError(Exception):
|
|
111
|
+
"""Raised when the sidecar metadata TSV file has formatting issues (duplicate columns, empty columns, commas in values, etc.)."""
|
|
112
|
+
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TaskUserError(Exception):
|
|
117
|
+
"""
|
|
118
|
+
Raised in Celery tasks when an error needs to propagate back to the CLI user.
|
|
119
|
+
|
|
120
|
+
This is intentionally kept as a simple Exception subclass (no custom __init__)
|
|
121
|
+
to avoid UnpicklableExceptionWrapper when passing through Celery's JSON serialization/deserialization.
|
|
122
|
+
Complex exception types such as those inheriting from DivBaseAPIException seem to trigger UnpicklableExceptionWrapper.
|
|
123
|
+
|
|
124
|
+
This class is essentially a wrapper to allow to use more complex exceptions in Celery tasks and catch them
|
|
125
|
+
in the API route handlers to return user-friendly error messages to the CLI.
|
|
126
|
+
In the Celery task use it like:
|
|
127
|
+
raise TaskUserError(str(SomeComplexError(...))) from None
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class NoVCFFilesFoundError(Exception):
|
|
134
|
+
"""Raised when no VCF files are found in the project bucket."""
|
|
135
|
+
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DimensionsNotUpToDateWithBucketError(Exception):
|
|
140
|
+
"""Raised when VCF files in a bucket are missing or outdated in the dimensions index."""
|
|
141
|
+
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ChecksumVerificationError(Exception):
|
|
146
|
+
"""Raised when a calculated file's checksum does not match the expected value."""
|
|
147
|
+
|
|
148
|
+
def __init__(self, expected_checksum: str, calculated_checksum: str):
|
|
149
|
+
self.expected_checksum = expected_checksum
|
|
150
|
+
self.calculated_checksum = calculated_checksum
|
|
151
|
+
|
|
152
|
+
message = (
|
|
153
|
+
f"Checksum verification failed. Expected: {expected_checksum}, Calculated: {calculated_checksum}"
|
|
154
|
+
f" The file has been deleted to avoid accidental use of a corrupted file."
|
|
155
|
+
)
|
|
156
|
+
super().__init__(message)
|