opencloning-db 1.4.0__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.
Files changed (62) hide show
  1. opencloning_db-1.4.0/.gitignore +30 -0
  2. opencloning_db-1.4.0/CHANGELOG.md +13 -0
  3. opencloning_db-1.4.0/PKG-INFO +91 -0
  4. opencloning_db-1.4.0/README.md +69 -0
  5. opencloning_db-1.4.0/pyproject.toml +48 -0
  6. opencloning_db-1.4.0/src/opencloning_db/__init__.py +3 -0
  7. opencloning_db-1.4.0/src/opencloning_db/api.py +60 -0
  8. opencloning_db-1.4.0/src/opencloning_db/apimodels.py +335 -0
  9. opencloning_db-1.4.0/src/opencloning_db/auth/__init__.py +1 -0
  10. opencloning_db-1.4.0/src/opencloning_db/auth/security.py +39 -0
  11. opencloning_db-1.4.0/src/opencloning_db/combined.py +28 -0
  12. opencloning_db-1.4.0/src/opencloning_db/config.py +92 -0
  13. opencloning_db-1.4.0/src/opencloning_db/context.py +15 -0
  14. opencloning_db-1.4.0/src/opencloning_db/db.py +149 -0
  15. opencloning_db-1.4.0/src/opencloning_db/deps.py +47 -0
  16. opencloning_db-1.4.0/src/opencloning_db/init_db/cre_lox_recombination.json +145 -0
  17. opencloning_db-1.4.0/src/opencloning_db/init_db/crispr_hdr.json +152 -0
  18. opencloning_db-1.4.0/src/opencloning_db/init_db/example_sequencing.json +31 -0
  19. opencloning_db-1.4.0/src/opencloning_db/init_db/gateway.json +198 -0
  20. opencloning_db-1.4.0/src/opencloning_db/init_db/gibson_assembly.json +188 -0
  21. opencloning_db-1.4.0/src/opencloning_db/init_db/golden_gate.json +313 -0
  22. opencloning_db-1.4.0/src/opencloning_db/init_db/homologous_recombination.json +141 -0
  23. opencloning_db-1.4.0/src/opencloning_db/init_db/restriction_ligation_assembly.json +139 -0
  24. opencloning_db-1.4.0/src/opencloning_db/init_db/restriction_then_ligation.json +195 -0
  25. opencloning_db-1.4.0/src/opencloning_db/init_db/sequencing_data/TN9W63_1_pREX0008.ab1 +0 -0
  26. opencloning_db-1.4.0/src/opencloning_db/init_db/sequencing_data/TN9W63_1_pREX0008.gbk +226 -0
  27. opencloning_db-1.4.0/src/opencloning_db/init_db/sequencing_data/TN9W63_1_pREX0008_mutations_added.fasta +71 -0
  28. opencloning_db-1.4.0/src/opencloning_db/init_db/templateless_PCR.json +71 -0
  29. opencloning_db-1.4.0/src/opencloning_db/init_db.py +199 -0
  30. opencloning_db-1.4.0/src/opencloning_db/models.py +831 -0
  31. opencloning_db-1.4.0/src/opencloning_db/routers/__init__.py +1 -0
  32. opencloning_db-1.4.0/src/opencloning_db/routers/auth.py +90 -0
  33. opencloning_db-1.4.0/src/opencloning_db/routers/lines.py +261 -0
  34. opencloning_db-1.4.0/src/opencloning_db/routers/primers.py +347 -0
  35. opencloning_db-1.4.0/src/opencloning_db/routers/sequence_samples.py +116 -0
  36. opencloning_db-1.4.0/src/opencloning_db/routers/sequences.py +823 -0
  37. opencloning_db-1.4.0/src/opencloning_db/routers/tags.py +246 -0
  38. opencloning_db-1.4.0/src/opencloning_db/routers/template_sequences.py +28 -0
  39. opencloning_db-1.4.0/src/opencloning_db/routers/test_tools.py +43 -0
  40. opencloning_db-1.4.0/src/opencloning_db/routers/workspaces.py +102 -0
  41. opencloning_db-1.4.0/src/opencloning_db/utils.py +22 -0
  42. opencloning_db-1.4.0/src/opencloning_db/workspace_auth.py +41 -0
  43. opencloning_db-1.4.0/src/opencloning_db/workspace_deps.py +161 -0
  44. opencloning_db-1.4.0/tests/__init__.py +1 -0
  45. opencloning_db-1.4.0/tests/cloning_strategy_examples.py +30 -0
  46. opencloning_db-1.4.0/tests/conftest.py +88 -0
  47. opencloning_db-1.4.0/tests/helpers.py +264 -0
  48. opencloning_db-1.4.0/tests/test_api_models.py +17 -0
  49. opencloning_db-1.4.0/tests/test_auth.py +231 -0
  50. opencloning_db-1.4.0/tests/test_combined.py +36 -0
  51. opencloning_db-1.4.0/tests/test_init_db.py +9 -0
  52. opencloning_db-1.4.0/tests/test_lines.py +703 -0
  53. opencloning_db-1.4.0/tests/test_models.py +968 -0
  54. opencloning_db-1.4.0/tests/test_primers.py +864 -0
  55. opencloning_db-1.4.0/tests/test_sequence_samples.py +380 -0
  56. opencloning_db-1.4.0/tests/test_sequences.py +1782 -0
  57. opencloning_db-1.4.0/tests/test_tags.py +530 -0
  58. opencloning_db-1.4.0/tests/test_template_sequences.py +110 -0
  59. opencloning_db-1.4.0/tests/test_test_tools.py +61 -0
  60. opencloning_db-1.4.0/tests/test_utils.py +26 -0
  61. opencloning_db-1.4.0/tests/test_workspace_deps.py +168 -0
  62. opencloning_db-1.4.0/tests/test_workspaces.py +191 -0
@@ -0,0 +1,30 @@
1
+ *.pyc
2
+ /__pycache__
3
+ .DS_Store
4
+ .ipynb_checkpoints
5
+ /.venv
6
+
7
+ # Test coverage
8
+ .coverage
9
+ coverage.xml
10
+
11
+
12
+ # Frontend build
13
+ frontend/
14
+
15
+ # Env secrets
16
+ .env.secret
17
+
18
+ batch_cloning_output/
19
+ .pytest_cache/
20
+
21
+ # build artifacts
22
+ dist/
23
+
24
+ bin/
25
+
26
+ # dummy certs for testing
27
+ certs/
28
+
29
+ # development database
30
+ dev_database/
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## [1.4.0](https://github.com/OpenCloning/OpenCloning_backend/compare/opencloning-db-v1.3.9...opencloning-db-v1.4.0) (2026-05-19)
4
+
5
+
6
+ ### Features
7
+
8
+ * change opencloning-db docker-compose to mount file-storage ([#459](https://github.com/OpenCloning/OpenCloning_backend/issues/459)) ([e8653fd](https://github.com/OpenCloning/OpenCloning_backend/commit/e8653fd2c143018c1c232986a5258b2e38f811bc))
9
+ * use multiple workers in prod with gunicorn ([#461](https://github.com/OpenCloning/OpenCloning_backend/issues/461)) ([b9cc010](https://github.com/OpenCloning/OpenCloning_backend/commit/b9cc01024a9bcbfe33a657064f96cc0816052104))
10
+
11
+ ## Changelog
12
+
13
+ All notable changes to this package will be documented in this file.
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: opencloning-db
3
+ Version: 1.4.0
4
+ Summary: Database for OpenCloning, a web application to generate molecular cloning strategies in json format, and share them with others.
5
+ Project-URL: Repository, https://github.com/OpenCloning/OpenCloning_backend
6
+ Author-email: Manuel Lera-Ramirez <manulera14@gmail.com>
7
+ License-Expression: MIT
8
+ Requires-Python: <4,>=3.11
9
+ Requires-Dist: fastapi-pagination<0.16,>=0.15.10
10
+ Requires-Dist: fastapi>=0.135.3
11
+ Requires-Dist: gunicorn>=26.0.0
12
+ Requires-Dist: opencloning-linkml>=1.0.0
13
+ Requires-Dist: opencloning==1.3.9
14
+ Requires-Dist: psycopg[binary]<4,>=3.2
15
+ Requires-Dist: pwdlib[argon2]<0.3,>=0.2.1
16
+ Requires-Dist: pydantic[email]>=2.7.1
17
+ Requires-Dist: pydna>=5.5.11
18
+ Requires-Dist: pyjwt<3,>=2.10.1
19
+ Requires-Dist: sqlalchemy<3,>=2.0.41
20
+ Requires-Dist: starlette>=1.0.0
21
+ Description-Content-Type: text/markdown
22
+
23
+ # opencloning-db
24
+
25
+ `opencloning-db` is the database/API companion package for the OpenCloning backend. It provides the app and local data workflows used for OpenCloning database features.
26
+
27
+ ## Run locally
28
+
29
+ From the repository root:
30
+
31
+ ```bash
32
+ # Install or update workspace dependencies
33
+ uv sync
34
+
35
+ # If you are using mac, you may have to stop any local Postgres instances running on port 5432
36
+ brew services stop postgresql
37
+
38
+ # Start local Postgres with dev/test/e2e databases
39
+ docker compose -f docker/docker-compose.postgres.yml up -d
40
+
41
+ # Load required local runtime config
42
+ source .env.dev
43
+
44
+ # Seed the local baseline
45
+ uv run opencloning-cli db seed
46
+
47
+ # Run both the cloning and the database API - this what the OpenCloningDB frontend expects
48
+ uv run uvicorn opencloning_db.combined:app --reload --reload-exclude='.venv'
49
+
50
+ # Run the opencloning-db API (only database, not cloning. This is not used when running with the frontend)
51
+ uv run uvicorn opencloning_db.api:app --reload --reload-exclude='.venv'
52
+ ```
53
+
54
+ That will serve the cloning API at [http://127.0.0.1:8000/cloning](http://127.0.0.1:8000/cloning) and the database API at [http://127.0.0.1:8001/db](http://127.0.0.1:8001/db). That's what the OpenCloningDB frontend expects.
55
+
56
+ ## Running tests locally
57
+
58
+ From the repository root:
59
+
60
+ ```bash
61
+ # Install or update workspace dependencies
62
+ uv sync
63
+
64
+ # Run the tests
65
+ uv run pytest packages/opencloning-db/tests -v -ks
66
+ ```
67
+
68
+ ## Frontend testing
69
+
70
+ Frontend testing using the database requires reseting after tests that modify the database. You can do this by calling the `/__test/reset-db` endpoint with the `X-Test-Reset-Token` header set to `RESET-TOKEN`. That endpoint is only available if the `OPENCLONING_TESTING` environment variable is set to `1`.
71
+
72
+ ## Building and running the Docker image
73
+
74
+ The Dockerfile is shared with the cloning app, and the build arg `APP_TARGET` determines which app to build. So you can build the image by running:
75
+
76
+ ```bash
77
+ docker build -f docker/opencloning.Dockerfile --build-arg APP_TARGET=db -t manulera/opencloningbackend-db .
78
+ ```
79
+
80
+ Then run it for development:
81
+
82
+ ```bash
83
+ # Create the file storage directories
84
+ mkdir -p docker/file_storage/sequence_files docker/file_storage/sequencing_files
85
+
86
+ # Run the containers
87
+ docker compose \
88
+ -f docker/docker-compose.postgres.yml \
89
+ -f docker/docker-compose.opencloning-db.yml \
90
+ up -d
91
+ ```
@@ -0,0 +1,69 @@
1
+ # opencloning-db
2
+
3
+ `opencloning-db` is the database/API companion package for the OpenCloning backend. It provides the app and local data workflows used for OpenCloning database features.
4
+
5
+ ## Run locally
6
+
7
+ From the repository root:
8
+
9
+ ```bash
10
+ # Install or update workspace dependencies
11
+ uv sync
12
+
13
+ # If you are using mac, you may have to stop any local Postgres instances running on port 5432
14
+ brew services stop postgresql
15
+
16
+ # Start local Postgres with dev/test/e2e databases
17
+ docker compose -f docker/docker-compose.postgres.yml up -d
18
+
19
+ # Load required local runtime config
20
+ source .env.dev
21
+
22
+ # Seed the local baseline
23
+ uv run opencloning-cli db seed
24
+
25
+ # Run both the cloning and the database API - this what the OpenCloningDB frontend expects
26
+ uv run uvicorn opencloning_db.combined:app --reload --reload-exclude='.venv'
27
+
28
+ # Run the opencloning-db API (only database, not cloning. This is not used when running with the frontend)
29
+ uv run uvicorn opencloning_db.api:app --reload --reload-exclude='.venv'
30
+ ```
31
+
32
+ That will serve the cloning API at [http://127.0.0.1:8000/cloning](http://127.0.0.1:8000/cloning) and the database API at [http://127.0.0.1:8001/db](http://127.0.0.1:8001/db). That's what the OpenCloningDB frontend expects.
33
+
34
+ ## Running tests locally
35
+
36
+ From the repository root:
37
+
38
+ ```bash
39
+ # Install or update workspace dependencies
40
+ uv sync
41
+
42
+ # Run the tests
43
+ uv run pytest packages/opencloning-db/tests -v -ks
44
+ ```
45
+
46
+ ## Frontend testing
47
+
48
+ Frontend testing using the database requires reseting after tests that modify the database. You can do this by calling the `/__test/reset-db` endpoint with the `X-Test-Reset-Token` header set to `RESET-TOKEN`. That endpoint is only available if the `OPENCLONING_TESTING` environment variable is set to `1`.
49
+
50
+ ## Building and running the Docker image
51
+
52
+ The Dockerfile is shared with the cloning app, and the build arg `APP_TARGET` determines which app to build. So you can build the image by running:
53
+
54
+ ```bash
55
+ docker build -f docker/opencloning.Dockerfile --build-arg APP_TARGET=db -t manulera/opencloningbackend-db .
56
+ ```
57
+
58
+ Then run it for development:
59
+
60
+ ```bash
61
+ # Create the file storage directories
62
+ mkdir -p docker/file_storage/sequence_files docker/file_storage/sequencing_files
63
+
64
+ # Run the containers
65
+ docker compose \
66
+ -f docker/docker-compose.postgres.yml \
67
+ -f docker/docker-compose.opencloning-db.yml \
68
+ up -d
69
+ ```
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "opencloning-db"
3
+ version = "1.4.0"
4
+ description = "Database for OpenCloning, a web application to generate molecular cloning strategies in json format, and share them with others."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Manuel Lera-Ramirez", email = "manulera14@gmail.com" },
9
+ ]
10
+ requires-python = ">=3.11,<4"
11
+ dependencies = [
12
+ "opencloning==1.3.9", # x-release-please-version
13
+ "fastapi>=0.135.3",
14
+ "fastapi-pagination>=0.15.10,<0.16",
15
+ "opencloning-linkml>=1.0.0",
16
+ "pydantic[email]>=2.7.1",
17
+ "psycopg[binary]>=3.2,<4",
18
+ "sqlalchemy>=2.0.41,<3",
19
+ "PyJWT>=2.10.1,<3",
20
+ "pwdlib[argon2]>=0.2.1,<0.3",
21
+ "pydna>=5.5.11",
22
+ "starlette>=1.0.0",
23
+ "gunicorn>=26.0.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Repository = "https://github.com/OpenCloning/OpenCloning_backend"
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/opencloning_db"]
35
+
36
+ [tool.uv.sources]
37
+ opencloning = { workspace = true }
38
+
39
+ [tool.black]
40
+ skip-string-normalization = true
41
+ line-length = 119
42
+
43
+ [tool.deptry]
44
+ known_first_party = ["opencloning_db"]
45
+ per_rule_ignores = { DEP002 = ["psycopg", "gunicorn"] }
46
+
47
+ [tool.hatch.metadata]
48
+ allow-direct-references = true
@@ -0,0 +1,3 @@
1
+ """opencloning_db package."""
2
+
3
+ """OpenCloning DB package namespace."""
@@ -0,0 +1,60 @@
1
+ """
2
+ OpenCloning API - main FastAPI application.
3
+ """
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from fastapi_pagination import add_pagination
8
+ import os
9
+ from starlette.types import ASGIApp
10
+
11
+ from opencloning.app_settings import settings as opencloning_settings
12
+ from opencloning_db.config import parse_bool
13
+ from opencloning_db.routers import (
14
+ auth,
15
+ lines,
16
+ primers,
17
+ sequence_samples,
18
+ sequences,
19
+ template_sequences,
20
+ tags,
21
+ test_tools,
22
+ workspaces,
23
+ )
24
+
25
+
26
+ def create_fastapi_app() -> FastAPI:
27
+ app = FastAPI(title='OpenCloningDB API')
28
+
29
+ app.include_router(auth.router)
30
+ app.include_router(workspaces.router)
31
+ app.include_router(tags.router)
32
+ app.include_router(primers.router)
33
+ app.include_router(sequences.router)
34
+ app.include_router(template_sequences.router)
35
+ app.include_router(lines.router)
36
+ app.include_router(sequence_samples.router)
37
+ if parse_bool(os.getenv('OPENCLONING_TESTING', False)):
38
+ app.include_router(test_tools.router)
39
+
40
+ # Register routes first so Page[...] endpoints get pagination_ctx.
41
+ add_pagination(app)
42
+ return app
43
+
44
+
45
+ def wrap_with_cors(app: ASGIApp) -> ASGIApp:
46
+ return CORSMiddleware(
47
+ app,
48
+ allow_origins=opencloning_settings.ALLOWED_ORIGINS,
49
+ allow_credentials=True,
50
+ allow_methods=['*'],
51
+ allow_headers=['*'],
52
+ )
53
+
54
+
55
+ def create_app() -> ASGIApp:
56
+ return wrap_with_cors(create_fastapi_app())
57
+
58
+
59
+ fastapi_app = create_fastapi_app()
60
+ app = wrap_with_cors(fastapi_app)
@@ -0,0 +1,335 @@
1
+ """Shared Pydantic request/response models for the API."""
2
+
3
+ from datetime import datetime
4
+
5
+ from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
6
+
7
+ import opencloning_linkml.datamodel.models as opencloning_models
8
+ from opencloning_db.models import BaseSequence, SequenceType, Sequence, Primer, Line, SequenceInLine
9
+
10
+
11
+ class ApiModel(BaseModel):
12
+ """Reject unknown JSON keys on all API request/response models in this module."""
13
+
14
+ model_config = ConfigDict(extra='forbid')
15
+
16
+
17
+ # --- Auth (OAuth2 password + JWT) ---
18
+ class Token(ApiModel):
19
+ access_token: str
20
+ token_type: str = 'bearer'
21
+
22
+
23
+ class UserPublic(ApiModel):
24
+ id: int
25
+ email: str
26
+ display_name: str | None
27
+ is_instance_admin: bool
28
+
29
+
30
+ class UserRef(ApiModel):
31
+ """Minimal user reference for embedding in resource responses."""
32
+
33
+ id: int
34
+ display_name: str | None
35
+
36
+
37
+ class WorkspaceRef(ApiModel):
38
+ id: int
39
+ name: str
40
+ role: str
41
+
42
+
43
+ class WorkspaceCreate(ApiModel):
44
+ name: str = Field(min_length=1)
45
+
46
+
47
+ class WorkspaceRename(ApiModel):
48
+ name: str = Field(min_length=1)
49
+
50
+
51
+ class RegisterBody(ApiModel):
52
+ email: EmailStr
53
+ password: str = Field(min_length=1)
54
+ display_name: str | None = None
55
+
56
+
57
+ # --- Sequence sample ---
58
+ class SequenceSampleCreate(ApiModel):
59
+ uid: str
60
+ sequence_id: int
61
+
62
+
63
+ class SequenceSampleUpdate(ApiModel):
64
+ sequence_id: int
65
+
66
+
67
+ class SequenceSampleRead(ApiModel):
68
+ id: int
69
+ uid: str
70
+ sequence_id: int
71
+
72
+
73
+ class SequenceSampleCreated(ApiModel):
74
+ id: int
75
+ uid: str
76
+
77
+
78
+ # --- Tags ---
79
+ class TagCreate(ApiModel):
80
+ name: str = Field(min_length=1)
81
+
82
+ @field_validator('name', mode='before')
83
+ @classmethod
84
+ def strip_tag_name(cls, v: object) -> object:
85
+ # We do it before to strip before counting the length of the string
86
+ if isinstance(v, str):
87
+ return v.strip()
88
+ return v
89
+
90
+
91
+ class TagRead(ApiModel):
92
+ id: int
93
+ name: str
94
+
95
+
96
+ class EntityTagAttach(ApiModel):
97
+ tag_id: int
98
+
99
+
100
+ # --- Entity refs ---
101
+ class InputEntityRef(ApiModel):
102
+ id: int
103
+ type: str
104
+ name: str | None
105
+
106
+
107
+ class SequencingFileRef(ApiModel):
108
+ id: int
109
+ original_name: str
110
+
111
+
112
+ # --- Common responses ---
113
+ class IdResponse(ApiModel):
114
+ id: int
115
+
116
+
117
+ class RemovedResponse(ApiModel):
118
+ removed: int
119
+
120
+
121
+ class DeletedResponse(ApiModel):
122
+ deleted: int
123
+ data: dict | None = None
124
+
125
+
126
+ # --- Cloning strategy ---
127
+ class CloningStrategyIdMapping(ApiModel):
128
+ localId: int
129
+ databaseId: int
130
+
131
+
132
+ class CloningStrategyResponse(ApiModel):
133
+ id: int
134
+ mappings: list[CloningStrategyIdMapping]
135
+
136
+
137
+ # --- Sequence / primer refs ---
138
+ class SequenceRef(ApiModel):
139
+ id: int
140
+ type: str
141
+ name: str | None
142
+ sequence_type: SequenceType
143
+ tags: list[TagRead] = []
144
+ sample_uids: list[str] = []
145
+ seguid: str | None = None
146
+ created_at: datetime
147
+ created_by: UserRef
148
+
149
+
150
+ class SequenceUpdate(ApiModel):
151
+ name: str | None = None
152
+ sequence_type: SequenceType | None = None
153
+
154
+
155
+ class TemplateSequenceCreate(ApiModel):
156
+ name: str = Field(min_length=1)
157
+ sequence_type: SequenceType
158
+
159
+ @field_validator('name', mode='before')
160
+ @classmethod
161
+ def strip_name(cls, v: object) -> object:
162
+ if isinstance(v, str):
163
+ return v.strip()
164
+ return v
165
+
166
+
167
+ class PrimerUpdate(ApiModel):
168
+ name: str | None = None
169
+ uid: str | None = None
170
+
171
+ @field_validator('uid', mode='before')
172
+ @classmethod
173
+ def strip_uid(cls, v: object) -> object:
174
+ if isinstance(v, str):
175
+ return v.strip()
176
+ return v
177
+
178
+ @field_validator('name', mode='before')
179
+ @classmethod
180
+ def strip_name(cls, v: object) -> object:
181
+ if isinstance(v, str):
182
+ stripped_name = v.strip()
183
+ if len(stripped_name) < 2:
184
+ raise ValueError('Primer name must be at least 2 characters long')
185
+ return stripped_name
186
+ return v
187
+
188
+
189
+ class PrimerCreate(ApiModel):
190
+ name: str
191
+ uid: str | None = None
192
+ sequence: str = Field(min_length=2, pattern=r'^[ACGTacgt]+$')
193
+
194
+
195
+ class PrimerBulkSubmission(ApiModel):
196
+ name: str
197
+ uid: str | None = None
198
+ sequence: str
199
+
200
+
201
+ class PrimerBulkRow(PrimerBulkSubmission):
202
+ sequence_invalid: bool
203
+ name_exists: bool
204
+ sequence_exists: bool
205
+ uid_exists: bool
206
+ name_duplicated: bool
207
+ sequence_duplicated: bool
208
+ uid_duplicated: bool
209
+
210
+
211
+ class PrimerRef(ApiModel):
212
+ id: int
213
+ name: str | None
214
+ sequence: str
215
+ uid: str | None = None
216
+ tags: list[TagRead] = []
217
+ created_at: datetime
218
+ created_by: UserRef
219
+
220
+
221
+ class SequenceSampleWithSequence(ApiModel):
222
+ id: int
223
+ uid: str
224
+ sequence_id: int
225
+ sequence: SequenceRef
226
+
227
+
228
+ # --- Line ---
229
+ class SequenceInLineRef(ApiModel):
230
+ """Sequence in a line, including the SequenceInLine instance id."""
231
+
232
+ id: int
233
+ sequence: SequenceRef
234
+
235
+
236
+ class LineRef(ApiModel):
237
+ id: int
238
+ uid: str
239
+ sequences_in_line: list[SequenceInLineRef]
240
+ parent_ids: list[int]
241
+ tags: list[TagRead]
242
+ created_at: datetime
243
+ created_by: UserRef
244
+
245
+
246
+ class LineCreate(ApiModel):
247
+ uid: str
248
+ allele_ids: list[int] = []
249
+ plasmid_ids: list[int] = []
250
+ parent_ids: list[int] = []
251
+
252
+
253
+ class LineUpdate(ApiModel):
254
+ uid: str | None = None
255
+ allele_ids: list[int] | None = None
256
+ plasmid_ids: list[int] | None = None
257
+ parent_ids: list[int] | None = None
258
+
259
+
260
+ def _user_ref(user) -> UserRef | None:
261
+ if user is None:
262
+ return None
263
+ return UserRef(id=user.id, display_name=user.display_name)
264
+
265
+
266
+ def sequence_ref(sequence: BaseSequence) -> SequenceRef:
267
+ return SequenceRef(
268
+ id=sequence.id,
269
+ type=sequence.type,
270
+ name=sequence.name,
271
+ sequence_type=sequence.sequence_type,
272
+ tags=[TagRead(id=t.id, name=t.name) for t in sequence.tags],
273
+ sample_uids=sequence.sample_uids,
274
+ seguid=sequence.seguid if isinstance(sequence, Sequence) else None,
275
+ created_at=sequence.created_at,
276
+ created_by=_user_ref(sequence.created_by),
277
+ )
278
+
279
+
280
+ def primer_ref(primer: Primer) -> PrimerRef:
281
+ return PrimerRef(
282
+ id=primer.id,
283
+ name=primer.name,
284
+ sequence=primer.sequence,
285
+ uid=primer.uid,
286
+ tags=[TagRead(id=t.id, name=t.name) for t in primer.tags],
287
+ created_at=primer.created_at,
288
+ created_by=_user_ref(primer.created_by),
289
+ )
290
+
291
+
292
+ def sequence_in_line_ref(sil: SequenceInLine) -> SequenceInLineRef:
293
+ """Build a SequenceInLineRef from a SequenceInLine ORM instance."""
294
+ seq = sil.sequence
295
+ return SequenceInLineRef(
296
+ id=sil.id,
297
+ sequence=sequence_ref(seq),
298
+ )
299
+
300
+
301
+ def line_ref(line: Line) -> LineRef:
302
+ return LineRef(
303
+ id=line.id,
304
+ uid=line.uid,
305
+ sequences_in_line=[sequence_in_line_ref(sil) for sil in line.sequences_in_line],
306
+ parent_ids=line.parent_ids,
307
+ tags=[TagRead(id=tag.id, name=tag.name) for tag in line.tags],
308
+ created_at=line.created_at,
309
+ created_by=_user_ref(line.created_by),
310
+ )
311
+
312
+
313
+ class SequenceSearchResult(ApiModel):
314
+ sequence_ref: SequenceRef
315
+ sequence: opencloning_models.TextFileSequence
316
+ shift: int = 0
317
+ reverse_complemented: bool = False
318
+
319
+
320
+ class SequenceValidationRow(ApiModel):
321
+ file_name: str
322
+ reading_error: bool
323
+
324
+ name: str | None = None
325
+ length: int | None = None
326
+ circular: bool | None = None
327
+ seguid: str | None = None
328
+ circularised_seguid: str | None = None
329
+
330
+ sequence_exists: bool | None = None
331
+ sequence_circularised_exists: bool | None = None
332
+ name_exists: bool | None = None
333
+
334
+ duplicated_seguid: bool | None = None
335
+ duplicated_name: bool | None = None
@@ -0,0 +1 @@
1
+ """Authentication helpers (password hashing, JWT)."""
@@ -0,0 +1,39 @@
1
+ """Password hashing (pwdlib) and JWT access tokens (PyJWT)."""
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+ from typing import Any
5
+
6
+ import jwt
7
+ from pwdlib import PasswordHash
8
+
9
+ from opencloning_db.config import Config
10
+
11
+ password_hasher = PasswordHash.recommended()
12
+
13
+
14
+ def get_password_hash(password: str) -> str:
15
+ return password_hasher.hash(password)
16
+
17
+
18
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
19
+ return password_hasher.verify(plain_password, hashed_password)
20
+
21
+
22
+ def create_access_token(data: dict[str, Any], config: Config, expires_delta: timedelta) -> str:
23
+ to_encode = data.copy()
24
+ expire = datetime.now(timezone.utc) + expires_delta
25
+ to_encode.update({'exp': expire})
26
+ encoded_jwt = jwt.encode(
27
+ to_encode,
28
+ config.jwt_secret,
29
+ algorithm=config.jwt_algorithm,
30
+ )
31
+ return encoded_jwt
32
+
33
+
34
+ def decode_access_token(token: str, config: Config) -> dict[str, Any]:
35
+ return jwt.decode(
36
+ token,
37
+ config.jwt_secret,
38
+ algorithms=[config.jwt_algorithm],
39
+ )