mgtx-benchling-wrapper 0.1.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.
- mgtx_benchling_wrapper-0.1.0/LICENSE +21 -0
- mgtx_benchling_wrapper-0.1.0/PKG-INFO +104 -0
- mgtx_benchling_wrapper-0.1.0/README.md +81 -0
- mgtx_benchling_wrapper-0.1.0/pyproject.toml +41 -0
- mgtx_benchling_wrapper-0.1.0/setup.cfg +4 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/__init__.py +1 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/client/__init__.py +0 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/client/benchling_client.py +67 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/context/__init__.py +0 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/context/benchling_context.py +56 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/__init__.py +0 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/chunking.py +6 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/compare_dataframe_cols.py +19 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/create_blob_payload.py +38 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/datetime_parser.py +43 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/deprecated.py +78 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/fields.py +15 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/list_unique_values_col.py +11 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/logger.py +38 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/substitute_df_col_dict.py +10 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/validation_summary.py +23 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/__init__.py +0 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/assays_result_ingestion.py +256 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/__init__.py +0 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/blob_handler.py +61 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/exceptions.py +19 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/result_archiver.py +183 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/result_ingestion.py +75 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/schema_handler.py +94 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/models/__init__.py +0 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/models/types.py +47 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/resolution/__init__.py +0 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/resolution/exceptions.py +21 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/resolution/links_resolver.py +70 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/resolution/schema_resolver.py +27 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/__init__.py +0 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/blob_transformer.py +94 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/container_transformer.py +290 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/datetime_converter.py +43 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/dropdown_transformer.py +74 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/exceptions.py +84 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/link_transformer.py +155 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/validation/__init__.py +0 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/validation/api_variable_validation.py +138 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/validation/dataframe_validator.py +203 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/validation/exceptions.py +190 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/validation/input_param_validator.py +151 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/__init__.py +0 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_assayresults.py +196 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_blobs.py +68 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_containers.py +128 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_customentities.py +242 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_dropdowns.py +56 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_entry.py +158 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_mixtures.py +117 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_projects.py +18 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_schemas.py +166 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_task.py +18 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/facade.py +33 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper.egg-info/PKG-INFO +104 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper.egg-info/SOURCES.txt +62 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper.egg-info/dependency_links.txt +1 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper.egg-info/requires.txt +10 -0
- mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ana Valinhas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mgtx-benchling-wrapper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python wrapper for Benchling API with common functions used at MGTX DSC
|
|
5
|
+
Author-email: Ana Valinhas <ana.valinhas@meiragtx.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: benchling-sdk>=1.21.2
|
|
14
|
+
Requires-Dist: benchling-api-client>=2.0.342
|
|
15
|
+
Requires-Dist: pandas>=2.2.2
|
|
16
|
+
Requires-Dist: numpy>=1.26.4
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-mock>=3.15.1; extra == "dev"
|
|
20
|
+
Requires-Dist: build; extra == "dev"
|
|
21
|
+
Requires-Dist: twine; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# mgtx-benchling-wrapper
|
|
25
|
+
A wrapper of the Benchling API with common functions and workflows used at MGTX DSC.
|
|
26
|
+
|
|
27
|
+
## Installation 🚀
|
|
28
|
+
You can install this package using:
|
|
29
|
+
```
|
|
30
|
+
pip install mgtx-benchling-wrapper
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quickstart 🚩
|
|
34
|
+
After creating an app on your Benchling tenant, create a config.yaml file in your repo.
|
|
35
|
+
The following are the contents of the config.yaml
|
|
36
|
+
```
|
|
37
|
+
BenchlingCredentials:
|
|
38
|
+
benchling_url: 'https://mytenant.benchling.com'
|
|
39
|
+
benchling_access_token: 'https://mytenant.benchling.com/api/v2/token'
|
|
40
|
+
app_client_id: 'your-app-client-id'
|
|
41
|
+
app_client_secret: 'encrypted-client-secret'
|
|
42
|
+
AssaySchema:
|
|
43
|
+
schema_id: "schema_api_id"
|
|
44
|
+
Project:
|
|
45
|
+
project_id: "project_api_id"
|
|
46
|
+
```
|
|
47
|
+
The app_client_secret should be encrypted. Create encryption.py Use cryptography as follows:
|
|
48
|
+
```
|
|
49
|
+
from cryptography.fernet import Fernet
|
|
50
|
+
|
|
51
|
+
def decrypt(value):
|
|
52
|
+
return f.decrypt(value.encode()).decode()
|
|
53
|
+
|
|
54
|
+
def encrypt(value):
|
|
55
|
+
return f.encrypt(value.encode()).decode()
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
# Run this to print an encrypted string of "my-value"
|
|
59
|
+
print(encrypt('your-app-client-secret))
|
|
60
|
+
```
|
|
61
|
+
The following is an example of the use of the assay_results_ingestion workflow.
|
|
62
|
+
```
|
|
63
|
+
import yaml
|
|
64
|
+
from encryption import decrypt
|
|
65
|
+
from mgtx_benchling_wrapper.context.benchling_context import BenchlingContext
|
|
66
|
+
from mgtx_benchling_wrapper.wrapper.facade import BenchlingWrapperFacade
|
|
67
|
+
from mgtx_benchling_wrapper.workflows.assays_result_ingestion import (
|
|
68
|
+
AssayResultIngestionWorkflow
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def config():
|
|
72
|
+
with open("tests/config/test_config.yml") as f:
|
|
73
|
+
return yaml.safe_load(f)
|
|
74
|
+
|
|
75
|
+
#create the benchling context
|
|
76
|
+
ctx = BenchlingContext(
|
|
77
|
+
base_url=config()['BenchlingCredentials']['benchling_url'],
|
|
78
|
+
client_id=config()['BenchlingCredentials']['app_client_id'],
|
|
79
|
+
client_secret=decrypt(config()['BenchlingCredentials']['app_client_secret']),
|
|
80
|
+
token_url=config()['BenchlingCredentials']['benchling_access_token'],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
#initialize the wrapper
|
|
84
|
+
wrapper = BenchlingWrapperFacade(ctx.benchling())
|
|
85
|
+
|
|
86
|
+
#retrieve assay_schema_id
|
|
87
|
+
schema_id = config()['AssaySchema']['schema_id']
|
|
88
|
+
|
|
89
|
+
#retrieve project_id
|
|
90
|
+
project_id = config()['Project']['project_id']
|
|
91
|
+
|
|
92
|
+
#initiate results ingestion workflow
|
|
93
|
+
results_ingestion = AssayResultIngestionWorkflow(wrapper)
|
|
94
|
+
|
|
95
|
+
#ingest results on benchling
|
|
96
|
+
list_missing_entities = results_ingestion.assay_results_ingestion_updated(
|
|
97
|
+
[dataframe_to_ingest],
|
|
98
|
+
schema_id,
|
|
99
|
+
project_id,
|
|
100
|
+
unique_identifiers =['assay_run_id', 'sample_id']
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# mgtx-benchling-wrapper
|
|
2
|
+
A wrapper of the Benchling API with common functions and workflows used at MGTX DSC.
|
|
3
|
+
|
|
4
|
+
## Installation 🚀
|
|
5
|
+
You can install this package using:
|
|
6
|
+
```
|
|
7
|
+
pip install mgtx-benchling-wrapper
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Quickstart 🚩
|
|
11
|
+
After creating an app on your Benchling tenant, create a config.yaml file in your repo.
|
|
12
|
+
The following are the contents of the config.yaml
|
|
13
|
+
```
|
|
14
|
+
BenchlingCredentials:
|
|
15
|
+
benchling_url: 'https://mytenant.benchling.com'
|
|
16
|
+
benchling_access_token: 'https://mytenant.benchling.com/api/v2/token'
|
|
17
|
+
app_client_id: 'your-app-client-id'
|
|
18
|
+
app_client_secret: 'encrypted-client-secret'
|
|
19
|
+
AssaySchema:
|
|
20
|
+
schema_id: "schema_api_id"
|
|
21
|
+
Project:
|
|
22
|
+
project_id: "project_api_id"
|
|
23
|
+
```
|
|
24
|
+
The app_client_secret should be encrypted. Create encryption.py Use cryptography as follows:
|
|
25
|
+
```
|
|
26
|
+
from cryptography.fernet import Fernet
|
|
27
|
+
|
|
28
|
+
def decrypt(value):
|
|
29
|
+
return f.decrypt(value.encode()).decode()
|
|
30
|
+
|
|
31
|
+
def encrypt(value):
|
|
32
|
+
return f.encrypt(value.encode()).decode()
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
# Run this to print an encrypted string of "my-value"
|
|
36
|
+
print(encrypt('your-app-client-secret))
|
|
37
|
+
```
|
|
38
|
+
The following is an example of the use of the assay_results_ingestion workflow.
|
|
39
|
+
```
|
|
40
|
+
import yaml
|
|
41
|
+
from encryption import decrypt
|
|
42
|
+
from mgtx_benchling_wrapper.context.benchling_context import BenchlingContext
|
|
43
|
+
from mgtx_benchling_wrapper.wrapper.facade import BenchlingWrapperFacade
|
|
44
|
+
from mgtx_benchling_wrapper.workflows.assays_result_ingestion import (
|
|
45
|
+
AssayResultIngestionWorkflow
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def config():
|
|
49
|
+
with open("tests/config/test_config.yml") as f:
|
|
50
|
+
return yaml.safe_load(f)
|
|
51
|
+
|
|
52
|
+
#create the benchling context
|
|
53
|
+
ctx = BenchlingContext(
|
|
54
|
+
base_url=config()['BenchlingCredentials']['benchling_url'],
|
|
55
|
+
client_id=config()['BenchlingCredentials']['app_client_id'],
|
|
56
|
+
client_secret=decrypt(config()['BenchlingCredentials']['app_client_secret']),
|
|
57
|
+
token_url=config()['BenchlingCredentials']['benchling_access_token'],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
#initialize the wrapper
|
|
61
|
+
wrapper = BenchlingWrapperFacade(ctx.benchling())
|
|
62
|
+
|
|
63
|
+
#retrieve assay_schema_id
|
|
64
|
+
schema_id = config()['AssaySchema']['schema_id']
|
|
65
|
+
|
|
66
|
+
#retrieve project_id
|
|
67
|
+
project_id = config()['Project']['project_id']
|
|
68
|
+
|
|
69
|
+
#initiate results ingestion workflow
|
|
70
|
+
results_ingestion = AssayResultIngestionWorkflow(wrapper)
|
|
71
|
+
|
|
72
|
+
#ingest results on benchling
|
|
73
|
+
list_missing_entities = results_ingestion.assay_results_ingestion_updated(
|
|
74
|
+
[dataframe_to_ingest],
|
|
75
|
+
schema_id,
|
|
76
|
+
project_id,
|
|
77
|
+
unique_identifiers =['assay_run_id', 'sample_id']
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mgtx-benchling-wrapper"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python wrapper for Benchling API with common functions used at MGTX DSC"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Ana Valinhas", email = "ana.valinhas@meiragtx.com" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"benchling-sdk>=1.21.2",
|
|
18
|
+
"benchling-api-client>=2.0.342",
|
|
19
|
+
"pandas>=2.2.2",
|
|
20
|
+
"numpy>=1.26.4"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Operating System :: OS Independent"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=9.0.3",
|
|
32
|
+
"pytest-mock>=3.15.1",
|
|
33
|
+
"build",
|
|
34
|
+
"twine"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
package-dir = {"" = "src"}
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.2"
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from benchling_sdk.benchling import Benchling
|
|
4
|
+
from benchling_api_client.benchling_client import BenchlingApiClient
|
|
5
|
+
from benchling_sdk.helpers.retry_helpers import RetryStrategy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# -----------------------------
|
|
9
|
+
# Configuration defaults
|
|
10
|
+
# -----------------------------
|
|
11
|
+
|
|
12
|
+
DEFAULT_TIMEOUT_SECONDS = 180
|
|
13
|
+
MAX_RETRIES = 10
|
|
14
|
+
BACKOFF_FACTOR = 2.0
|
|
15
|
+
|
|
16
|
+
# -----------------------------
|
|
17
|
+
# Client decorators
|
|
18
|
+
# -----------------------------
|
|
19
|
+
|
|
20
|
+
def with_default_timeout(client: BenchlingApiClient) -> BenchlingApiClient:
|
|
21
|
+
"""
|
|
22
|
+
Apply default timeout to all Benchling API calls.
|
|
23
|
+
"""
|
|
24
|
+
return client.with_timeout(DEFAULT_TIMEOUT_SECONDS)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# -----------------------------
|
|
28
|
+
# Factory
|
|
29
|
+
# -----------------------------
|
|
30
|
+
|
|
31
|
+
def with_backoff_timeout():
|
|
32
|
+
return RetryStrategy(
|
|
33
|
+
max_tries=MAX_RETRIES,
|
|
34
|
+
backoff_factor=BACKOFF_FACTOR
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def create_benchling_client(
|
|
38
|
+
*,
|
|
39
|
+
url: str,
|
|
40
|
+
auth,
|
|
41
|
+
client_decorator: Callable[[BenchlingApiClient], BenchlingApiClient] = with_default_timeout,
|
|
42
|
+
) -> Benchling:
|
|
43
|
+
"""
|
|
44
|
+
Factory for creating a Benchling SDK client.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
url (str):
|
|
48
|
+
Benchling tenant base URL (e.g. https://acme.benchling.com)
|
|
49
|
+
|
|
50
|
+
auth:
|
|
51
|
+
Authentication method compatible with Benchling SDK
|
|
52
|
+
(e.g. ClientCredentialsOAuth2)
|
|
53
|
+
|
|
54
|
+
client_decorator (callable):
|
|
55
|
+
Optional decorator to customize the underlying API client
|
|
56
|
+
(timeouts, retries, logging, etc.)
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Benchling:
|
|
60
|
+
Configured Benchling SDK client
|
|
61
|
+
"""
|
|
62
|
+
return Benchling(
|
|
63
|
+
url=url,
|
|
64
|
+
auth_method=auth,
|
|
65
|
+
client_decorator=client_decorator,
|
|
66
|
+
retry_strategy=with_backoff_timeout(),
|
|
67
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from functools import cache
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from benchling_sdk.benchling import Benchling
|
|
5
|
+
from benchling_sdk.apps.framework import App
|
|
6
|
+
from benchling_sdk.auth.client_credentials_oauth2 import ClientCredentialsOAuth2
|
|
7
|
+
from benchling_sdk.models.webhooks.v0 import WebhookEnvelopeV0
|
|
8
|
+
|
|
9
|
+
from mgtx_benchling_wrapper.client.benchling_client import create_benchling_client
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BenchlingContext:
|
|
13
|
+
"""
|
|
14
|
+
Unified authentication + tenant context.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
client_id: str,
|
|
21
|
+
client_secret: str,
|
|
22
|
+
base_url: str,
|
|
23
|
+
token_url: Optional[str] = None,
|
|
24
|
+
app_id: Optional[str] = None,
|
|
25
|
+
):
|
|
26
|
+
self._client_id = client_id
|
|
27
|
+
self._client_secret = client_secret
|
|
28
|
+
self._base_url = base_url
|
|
29
|
+
self._token_url = token_url
|
|
30
|
+
self._app_id = app_id
|
|
31
|
+
|
|
32
|
+
@cache
|
|
33
|
+
def _auth(self):
|
|
34
|
+
return ClientCredentialsOAuth2(self._client_id, self._client_secret, self._token_url)
|
|
35
|
+
|
|
36
|
+
@cache
|
|
37
|
+
def benchling(self) -> Benchling:
|
|
38
|
+
return create_benchling_client(
|
|
39
|
+
url=self._base_url,
|
|
40
|
+
auth=self._auth(),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@cache
|
|
44
|
+
def app(self) -> App:
|
|
45
|
+
if self._app_id is None:
|
|
46
|
+
raise RuntimeError("App requested but no app_id provided")
|
|
47
|
+
return App(self._app_id, self.benchling())
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_webhook(cls, webhook: WebhookEnvelopeV0, *, client_id, client_secret):
|
|
51
|
+
return cls(
|
|
52
|
+
client_id=client_id,
|
|
53
|
+
client_secret=client_secret,
|
|
54
|
+
base_url=webhook.base_url,
|
|
55
|
+
app_id=webhook.app.id,
|
|
56
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
def compare_dataframe_columns(
|
|
4
|
+
dataframe: pd.DataFrame,
|
|
5
|
+
new_dataframe: pd.DataFrame,
|
|
6
|
+
column_name: str
|
|
7
|
+
) -> list[str]:
|
|
8
|
+
|
|
9
|
+
list_missing_values = []
|
|
10
|
+
org_column = list(dataframe[column_name])
|
|
11
|
+
new_column = list(new_dataframe[column_name])
|
|
12
|
+
|
|
13
|
+
for value in org_column:
|
|
14
|
+
if value in new_column:
|
|
15
|
+
if value not in list_missing_values:
|
|
16
|
+
if value is not None:
|
|
17
|
+
list_missing_values.append(value)
|
|
18
|
+
|
|
19
|
+
return list_missing_values
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import os.path
|
|
4
|
+
|
|
5
|
+
def create_image_blob_payload(
|
|
6
|
+
image_path: str,
|
|
7
|
+
mime_type="image/png",
|
|
8
|
+
blob_type="VISUALIZATION"):
|
|
9
|
+
"""
|
|
10
|
+
Create a payload for uploading an image blob through the API.
|
|
11
|
+
"""
|
|
12
|
+
try:
|
|
13
|
+
with open(image_path, "rb") as image_file:
|
|
14
|
+
image_data = image_file.read()
|
|
15
|
+
except FileNotFoundError:
|
|
16
|
+
raise Exception(f"Blob file not found: {image_path}")
|
|
17
|
+
|
|
18
|
+
#get name of the image
|
|
19
|
+
_,tail = os.path.split(image_path)
|
|
20
|
+
|
|
21
|
+
name = tail.split(".")[0]
|
|
22
|
+
|
|
23
|
+
# Encode the binary data to base64
|
|
24
|
+
data64 = base64.b64encode(image_data).decode('utf-8')
|
|
25
|
+
|
|
26
|
+
# Calculate the MD5 hash of the image data
|
|
27
|
+
md5_hash = hashlib.md5(image_data).hexdigest()
|
|
28
|
+
|
|
29
|
+
# Create the payload
|
|
30
|
+
payload = {
|
|
31
|
+
"data64": data64,
|
|
32
|
+
"md5": md5_hash,
|
|
33
|
+
"mimeType": mime_type,
|
|
34
|
+
"name": name,
|
|
35
|
+
"type": blob_type
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return payload, name
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import re
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
def _parse_datetime(value):
|
|
7
|
+
if pd.isna(value):
|
|
8
|
+
return None
|
|
9
|
+
|
|
10
|
+
if isinstance(value, (pd.Timestamp, datetime.date)):
|
|
11
|
+
return pd.Timestamp(value)
|
|
12
|
+
|
|
13
|
+
if _is_ambiguous_date(value):
|
|
14
|
+
warnings.warn(
|
|
15
|
+
f"Ambiguous date format detected '{value}'. "
|
|
16
|
+
f"Assuming day-first format (DD/MM/YYYY).",
|
|
17
|
+
UserWarning
|
|
18
|
+
)
|
|
19
|
+
return pd.to_datetime(value, errors="raise", dayfirst=True, format='mixed')
|
|
20
|
+
|
|
21
|
+
return pd.to_datetime(
|
|
22
|
+
value,
|
|
23
|
+
errors="raise",
|
|
24
|
+
dayfirst=True
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_ambiguous_date(value: str) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Detect ambiguous numeric date formats like 01/02/2024
|
|
31
|
+
where both day and month <= 12.
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(value, str):
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
pattern = r"^\d{1,2}[/-]\d{1,2}[/-]\d{4}$"
|
|
37
|
+
if not re.match(pattern, value.strip()):
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
parts = re.split(r"[/-]", value)
|
|
41
|
+
day, month, _ = map(int, parts)
|
|
42
|
+
|
|
43
|
+
return day <= 12 and month <= 12
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
string_types = (type(b''), type(u''))
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def deprecated(reason):
|
|
9
|
+
"""
|
|
10
|
+
This is a decorator which can be used to mark functions
|
|
11
|
+
as deprecated. It will result in a warning being emitted
|
|
12
|
+
when the function is used.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
if isinstance(reason, string_types):
|
|
16
|
+
|
|
17
|
+
# The @deprecated is used with a 'reason'.
|
|
18
|
+
#
|
|
19
|
+
# .. code-block:: python
|
|
20
|
+
#
|
|
21
|
+
# @deprecated("please, use another function")
|
|
22
|
+
# def old_function(x, y):
|
|
23
|
+
# pass
|
|
24
|
+
|
|
25
|
+
def decorator(func1):
|
|
26
|
+
|
|
27
|
+
if inspect.isclass(func1):
|
|
28
|
+
fmt1 = "Call to deprecated class {name} ({reason})."
|
|
29
|
+
else:
|
|
30
|
+
fmt1 = "Call to deprecated function {name} ({reason})."
|
|
31
|
+
|
|
32
|
+
@functools.wraps(func1)
|
|
33
|
+
def new_func1(*args, **kwargs):
|
|
34
|
+
warnings.simplefilter('always', DeprecationWarning)
|
|
35
|
+
warnings.warn(
|
|
36
|
+
fmt1.format(name=func1.__name__, reason=reason),
|
|
37
|
+
category=DeprecationWarning,
|
|
38
|
+
stacklevel=2
|
|
39
|
+
)
|
|
40
|
+
warnings.simplefilter('default', DeprecationWarning)
|
|
41
|
+
return func1(*args, **kwargs)
|
|
42
|
+
|
|
43
|
+
return new_func1
|
|
44
|
+
|
|
45
|
+
return decorator
|
|
46
|
+
|
|
47
|
+
elif inspect.isclass(reason) or inspect.isfunction(reason):
|
|
48
|
+
|
|
49
|
+
# The @deprecated is used without any 'reason'.
|
|
50
|
+
#
|
|
51
|
+
# .. code-block:: python
|
|
52
|
+
#
|
|
53
|
+
# @deprecated
|
|
54
|
+
# def old_function(x, y):
|
|
55
|
+
# pass
|
|
56
|
+
|
|
57
|
+
func2 = reason
|
|
58
|
+
|
|
59
|
+
if inspect.isclass(func2):
|
|
60
|
+
fmt2 = "Call to deprecated class {name}."
|
|
61
|
+
else:
|
|
62
|
+
fmt2 = "Call to deprecated function {name}."
|
|
63
|
+
|
|
64
|
+
@functools.wraps(func2)
|
|
65
|
+
def new_func2(*args, **kwargs):
|
|
66
|
+
warnings.simplefilter('always', DeprecationWarning)
|
|
67
|
+
warnings.warn(
|
|
68
|
+
fmt2.format(name=func2.__name__),
|
|
69
|
+
category=DeprecationWarning,
|
|
70
|
+
stacklevel=2
|
|
71
|
+
)
|
|
72
|
+
warnings.simplefilter('default', DeprecationWarning)
|
|
73
|
+
return func2(*args, **kwargs)
|
|
74
|
+
|
|
75
|
+
return new_func2
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
raise TypeError(repr(type(reason)))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
def create_fields_dict(
|
|
2
|
+
list_field_names: list,
|
|
3
|
+
list_field_values: list
|
|
4
|
+
) -> dict:
|
|
5
|
+
""" Creates a dictionary with the format {field_name:'value':{field_value}}
|
|
6
|
+
Args:
|
|
7
|
+
list_field_names (list): list containing the field names (usually the column names of dataframe)
|
|
8
|
+
list_field_values (list): values of the fields (usually a row in a dataframe)
|
|
9
|
+
Return:
|
|
10
|
+
final_dict (dict): a dictionary of the format to input in fields.
|
|
11
|
+
"""
|
|
12
|
+
list_word_value = ['value'] * len(list_field_names)
|
|
13
|
+
final_dict = {u: {v: w} for (u, v, w) in zip(list_field_names, list_word_value, list_field_values)}
|
|
14
|
+
|
|
15
|
+
return final_dict
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
def list_unique_values_in_column(
|
|
4
|
+
dataframe: pd.DataFrame,
|
|
5
|
+
column_name: str,
|
|
6
|
+
drop_na: bool = False
|
|
7
|
+
) -> list[str]:
|
|
8
|
+
if drop_na:
|
|
9
|
+
return dataframe[column_name].dropna().unique().tolist()
|
|
10
|
+
else:
|
|
11
|
+
return dataframe[column_name].unique().tolist()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
def get_logger(name: str, file_log_level: str | int, console_log_level: str | int, console_filter: str | None = None) -> logging.Logger:
|
|
4
|
+
logger = logging.getLogger(name)
|
|
5
|
+
logger.setLevel(logging.DEBUG)
|
|
6
|
+
|
|
7
|
+
# Factory function for creating level filters
|
|
8
|
+
def level_filter(level_name: str):
|
|
9
|
+
def filter_func(record):
|
|
10
|
+
return record.levelname == level_name
|
|
11
|
+
return filter_func
|
|
12
|
+
|
|
13
|
+
# Create formatters
|
|
14
|
+
console_format = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | line:%(lineno)d | %(message)s')
|
|
15
|
+
file_format = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# initiating a console handler
|
|
19
|
+
console_handler = logging.StreamHandler()
|
|
20
|
+
console_handler.setLevel((console_log_level))
|
|
21
|
+
console_handler.setFormatter(console_format)
|
|
22
|
+
# Add filter if specified
|
|
23
|
+
if console_filter:
|
|
24
|
+
console_handler.addFilter(level_filter(console_filter))
|
|
25
|
+
|
|
26
|
+
# initiating a file handler
|
|
27
|
+
file_handler = logging.FileHandler(
|
|
28
|
+
filename='app.log',
|
|
29
|
+
mode='a',
|
|
30
|
+
encoding='utf-8')
|
|
31
|
+
file_handler.setLevel((file_log_level))
|
|
32
|
+
file_handler.setFormatter(file_format)
|
|
33
|
+
|
|
34
|
+
# add the handlers to the logger
|
|
35
|
+
logger.addHandler(console_handler)
|
|
36
|
+
logger.addHandler(file_handler)
|
|
37
|
+
|
|
38
|
+
return logger
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from mgtx_benchling_wrapper.workflows.models.types import ValidationResult
|
|
2
|
+
|
|
3
|
+
def get_validation_summary(result: ValidationResult) -> str:
|
|
4
|
+
"""Generate human-readable validation summary."""
|
|
5
|
+
if result.is_valid:
|
|
6
|
+
summary = "✓ Validation passed"
|
|
7
|
+
if result.warnings:
|
|
8
|
+
summary += f" with {len(result.warnings)} warning(s)"
|
|
9
|
+
return summary
|
|
10
|
+
|
|
11
|
+
summary = f"✗ Validation failed with {len(result.errors)} error(s):\n"
|
|
12
|
+
|
|
13
|
+
for i, error in enumerate(result.errors, 1):
|
|
14
|
+
summary += f"\n{i}. {error.message}"
|
|
15
|
+
if hasattr(error, 'context') and error.context:
|
|
16
|
+
summary += f"\n Context: {error.context}"
|
|
17
|
+
|
|
18
|
+
if result.warnings:
|
|
19
|
+
summary += f"\n\nWarnings ({len(result.warnings)}):\n"
|
|
20
|
+
for warning in result.warnings:
|
|
21
|
+
summary += f" - {warning}\n"
|
|
22
|
+
|
|
23
|
+
return summary
|
|
File without changes
|