divbase-lib 0.1.0a1__tar.gz
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-0.1.0a1/.gitignore +46 -0
- divbase_lib-0.1.0a1/PKG-INFO +38 -0
- divbase_lib-0.1.0a1/README.md +11 -0
- divbase_lib-0.1.0a1/pyproject.toml +49 -0
- divbase_lib-0.1.0a1/src/divbase_lib/__init__.py +1 -0
- divbase_lib-0.1.0a1/src/divbase_lib/api_schemas/__init__.py +0 -0
- divbase_lib-0.1.0a1/src/divbase_lib/api_schemas/announcements.py +18 -0
- divbase_lib-0.1.0a1/src/divbase_lib/api_schemas/auth.py +34 -0
- divbase_lib-0.1.0a1/src/divbase_lib/api_schemas/personal_access_tokens.py +42 -0
- divbase_lib-0.1.0a1/src/divbase_lib/api_schemas/project_versions.py +87 -0
- divbase_lib-0.1.0a1/src/divbase_lib/api_schemas/queries.py +209 -0
- divbase_lib-0.1.0a1/src/divbase_lib/api_schemas/s3.py +214 -0
- divbase_lib-0.1.0a1/src/divbase_lib/api_schemas/task_history.py +50 -0
- divbase_lib-0.1.0a1/src/divbase_lib/api_schemas/vcf_dimensions.py +67 -0
- divbase_lib-0.1.0a1/src/divbase_lib/divbase_constants.py +56 -0
- divbase_lib-0.1.0a1/src/divbase_lib/exceptions.py +156 -0
- divbase_lib-0.1.0a1/src/divbase_lib/metadata_validator.py +827 -0
- divbase_lib-0.1.0a1/src/divbase_lib/s3_checksums.py +120 -0
- divbase_lib-0.1.0a1/src/divbase_lib/utils.py +76 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# repo secrets
|
|
2
|
+
.env
|
|
3
|
+
.env*
|
|
4
|
+
|
|
5
|
+
# python stuff
|
|
6
|
+
.venv
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*$py.class
|
|
11
|
+
|
|
12
|
+
.vscode/
|
|
13
|
+
|
|
14
|
+
# project specific files
|
|
15
|
+
/sample_metadata.tsv
|
|
16
|
+
/sample_metadata_*.tsv
|
|
17
|
+
*.vcf
|
|
18
|
+
*.vcf.gz
|
|
19
|
+
*.vcf.gz.csi
|
|
20
|
+
*.vcf.gz.tbi
|
|
21
|
+
!tests/fixtures/*.vcf.gz
|
|
22
|
+
tests/fixtures/temp*
|
|
23
|
+
tests/fixtures/merged*
|
|
24
|
+
divbase_metadata_template*.tsv
|
|
25
|
+
|
|
26
|
+
# query job config files
|
|
27
|
+
bcftools_divbase_job_config.json
|
|
28
|
+
|
|
29
|
+
# benchmarking files
|
|
30
|
+
vcf_dimensions.tsv
|
|
31
|
+
mock*.tsv
|
|
32
|
+
task_records*.json
|
|
33
|
+
split_scaffold_files.txt
|
|
34
|
+
scripts/benchmarking/*.yaml
|
|
35
|
+
scripts/benchmarking/results
|
|
36
|
+
|
|
37
|
+
#MacOS artifacts
|
|
38
|
+
.DS_Store
|
|
39
|
+
|
|
40
|
+
# mkdocs build cache
|
|
41
|
+
.cache/
|
|
42
|
+
# mkdocs auto generated files
|
|
43
|
+
docs/cli/_auto_generated/*.md
|
|
44
|
+
|
|
45
|
+
# pypi
|
|
46
|
+
dist/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: divbase-lib
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Library module for Divbase
|
|
5
|
+
Project-URL: Homepage, https://divbase.scilifelab-2-prod.sys.kth.se
|
|
6
|
+
Project-URL: Documentation, https://scilifelabdatacentre.github.io/divbase
|
|
7
|
+
Project-URL: Repository, https://github.com/ScilifelabDataCentre/divbase
|
|
8
|
+
Project-URL: Issues, https://github.com/ScilifelabDataCentre/divbase/issues
|
|
9
|
+
Author-email: SciLifeLab Data Centre <datacentre@scilifelab.se>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: httpx==0.28.1
|
|
22
|
+
Requires-Dist: pandas==2.3.3
|
|
23
|
+
Requires-Dist: pydantic==2.12.5
|
|
24
|
+
Requires-Dist: pyyaml==6.0.3
|
|
25
|
+
Requires-Dist: stamina==25.2.0
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# divbase-lib
|
|
29
|
+
|
|
30
|
+
This is library package for Divbase. Code from this library is shared between the Divbase CLI and API packages.
|
|
31
|
+
|
|
32
|
+
## Installation and usage
|
|
33
|
+
|
|
34
|
+
It is highly unlikely that you will need to use/install this package directly. Instead, it will be installed automatically by other packages (e.g. Divbase CLI) as a dependency.
|
|
35
|
+
|
|
36
|
+
## Development
|
|
37
|
+
|
|
38
|
+
This package is developed in the [DivBase repository](https://github.com/ScilifelabDataCentre/divbase) by Scilifelab Data Centre.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# divbase-lib
|
|
2
|
+
|
|
3
|
+
This is library package for Divbase. Code from this library is shared between the Divbase CLI and API packages.
|
|
4
|
+
|
|
5
|
+
## Installation and usage
|
|
6
|
+
|
|
7
|
+
It is highly unlikely that you will need to use/install this package directly. Instead, it will be installed automatically by other packages (e.g. Divbase CLI) as a dependency.
|
|
8
|
+
|
|
9
|
+
## Development
|
|
10
|
+
|
|
11
|
+
This package is developed in the [DivBase repository](https://github.com/ScilifelabDataCentre/divbase) by Scilifelab Data Centre.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "divbase-lib"
|
|
3
|
+
description = "Library module for Divbase"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "SciLifeLab Data Centre", email = "datacentre@scilifelab.se" },
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pyyaml==6.0.3",
|
|
12
|
+
"pandas==2.3.3", # TODO - consider major version bump
|
|
13
|
+
"pydantic==2.12.5",
|
|
14
|
+
"httpx==0.28.1",
|
|
15
|
+
"stamina==25.2.0"
|
|
16
|
+
]
|
|
17
|
+
dynamic = ["version"]
|
|
18
|
+
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Environment :: Console",
|
|
22
|
+
"Intended Audience :: Science/Research",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Programming Language :: Python :: 3.14",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://divbase.scilifelab-2-prod.sys.kth.se"
|
|
33
|
+
Documentation = "https://scilifelabdatacentre.github.io/divbase"
|
|
34
|
+
Repository = "https://github.com/ScilifelabDataCentre/divbase"
|
|
35
|
+
Issues = "https://github.com/ScilifelabDataCentre/divbase/issues"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["hatchling >= 1.26"]
|
|
39
|
+
build-backend = "hatchling.build"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.version]
|
|
42
|
+
path = "src/divbase_lib/__init__.py"
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
extend = "../../ruff.toml"
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
|
48
|
+
"divbase_cli" = { msg = "divbase-lib cannot import from divbase-cli, only the other way around works" }
|
|
49
|
+
"divbase_api" = { msg = "divbase-lib cannot import from divbase-api, only the other way around works" }
|
|
@@ -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
|