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.
Files changed (64) hide show
  1. mgtx_benchling_wrapper-0.1.0/LICENSE +21 -0
  2. mgtx_benchling_wrapper-0.1.0/PKG-INFO +104 -0
  3. mgtx_benchling_wrapper-0.1.0/README.md +81 -0
  4. mgtx_benchling_wrapper-0.1.0/pyproject.toml +41 -0
  5. mgtx_benchling_wrapper-0.1.0/setup.cfg +4 -0
  6. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/__init__.py +1 -0
  7. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/client/__init__.py +0 -0
  8. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/client/benchling_client.py +67 -0
  9. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/context/__init__.py +0 -0
  10. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/context/benchling_context.py +56 -0
  11. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/__init__.py +0 -0
  12. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/chunking.py +6 -0
  13. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/compare_dataframe_cols.py +19 -0
  14. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/create_blob_payload.py +38 -0
  15. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/datetime_parser.py +43 -0
  16. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/deprecated.py +78 -0
  17. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/fields.py +15 -0
  18. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/list_unique_values_col.py +11 -0
  19. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/logger.py +38 -0
  20. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/substitute_df_col_dict.py +10 -0
  21. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/utils/validation_summary.py +23 -0
  22. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/__init__.py +0 -0
  23. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/assays_result_ingestion.py +256 -0
  24. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/__init__.py +0 -0
  25. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/blob_handler.py +61 -0
  26. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/exceptions.py +19 -0
  27. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/result_archiver.py +183 -0
  28. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/result_ingestion.py +75 -0
  29. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/handlers/schema_handler.py +94 -0
  30. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/models/__init__.py +0 -0
  31. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/models/types.py +47 -0
  32. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/resolution/__init__.py +0 -0
  33. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/resolution/exceptions.py +21 -0
  34. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/resolution/links_resolver.py +70 -0
  35. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/resolution/schema_resolver.py +27 -0
  36. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/__init__.py +0 -0
  37. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/blob_transformer.py +94 -0
  38. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/container_transformer.py +290 -0
  39. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/datetime_converter.py +43 -0
  40. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/dropdown_transformer.py +74 -0
  41. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/exceptions.py +84 -0
  42. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/transformation/link_transformer.py +155 -0
  43. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/validation/__init__.py +0 -0
  44. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/validation/api_variable_validation.py +138 -0
  45. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/validation/dataframe_validator.py +203 -0
  46. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/validation/exceptions.py +190 -0
  47. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/workflows/validation/input_param_validator.py +151 -0
  48. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/__init__.py +0 -0
  49. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_assayresults.py +196 -0
  50. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_blobs.py +68 -0
  51. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_containers.py +128 -0
  52. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_customentities.py +242 -0
  53. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_dropdowns.py +56 -0
  54. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_entry.py +158 -0
  55. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_mixtures.py +117 -0
  56. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_projects.py +18 -0
  57. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_schemas.py +166 -0
  58. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/_task.py +18 -0
  59. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper/wrapper/facade.py +33 -0
  60. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper.egg-info/PKG-INFO +104 -0
  61. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper.egg-info/SOURCES.txt +62 -0
  62. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper.egg-info/dependency_links.txt +1 -0
  63. mgtx_benchling_wrapper-0.1.0/src/mgtx_benchling_wrapper.egg-info/requires.txt +10 -0
  64. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.2"
@@ -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
+ )
@@ -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
+ )
@@ -0,0 +1,6 @@
1
+ def chunk_list(lst: list, chunk_n: int):
2
+ """
3
+ Chunking the list into chunks of chunk_n elements.
4
+ """
5
+ for i in range(0, len(lst), chunk_n):
6
+ yield lst[i:i + chunk_n]
@@ -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,10 @@
1
+ import pandas as pd
2
+
3
+ def substitute_with_dict(
4
+ dataframe: pd.DataFrame,
5
+ column_name: str,
6
+ dict_orig_to_api: dict
7
+ ) -> pd.DataFrame:
8
+ dataframe[column_name] = dataframe[column_name].replace(dict_orig_to_api)
9
+
10
+ return dataframe
@@ -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