pybiolib 1.1.2189__tar.gz → 1.2.12__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 (121) hide show
  1. {pybiolib-1.1.2189 → pybiolib-1.2.12}/PKG-INFO +1 -1
  2. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/data_record/data_record.py +2 -0
  3. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/http_client.py +4 -2
  4. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/push_application.py +5 -0
  5. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/runtime.py +1 -0
  6. pybiolib-1.2.12/biolib/_internal/utils/multinode.py +264 -0
  7. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_runtime/runtime.py +4 -0
  8. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_api_client/app_types.py +1 -0
  9. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/cli/data_record.py +2 -0
  10. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/executors/docker_executor.py +1 -0
  11. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/job_storage.py +14 -1
  12. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/remote_host_proxy.py +32 -21
  13. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/webserver/worker_thread.py +42 -39
  14. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/utils/seq_util.py +1 -1
  15. {pybiolib-1.1.2189 → pybiolib-1.2.12}/pyproject.toml +3 -3
  16. {pybiolib-1.1.2189 → pybiolib-1.2.12}/LICENSE +0 -0
  17. /pybiolib-1.1.2189/README.md → /pybiolib-1.2.12/PYPI_README.md +0 -0
  18. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/__init__.py +0 -0
  19. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_data_record/data_record.py +0 -0
  20. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/__init__.py +0 -0
  21. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/data_record/__init__.py +0 -0
  22. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/data_record/remote_storage_endpoint.py +0 -0
  23. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/file_utils.py +0 -0
  24. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/fuse_mount/__init__.py +0 -0
  25. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/fuse_mount/experiment_fuse_mount.py +0 -0
  26. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/lfs/__init__.py +0 -0
  27. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/lfs/cache.py +0 -0
  28. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/libs/__init__.py +0 -0
  29. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/libs/fusepy/__init__.py +0 -0
  30. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/types/__init__.py +0 -0
  31. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/types/app.py +0 -0
  32. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/types/data_record.py +0 -0
  33. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/types/experiment.py +0 -0
  34. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/types/resource.py +0 -0
  35. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/types/typing.py +0 -0
  36. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/_internal/utils/__init__.py +0 -0
  37. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/api/__init__.py +0 -0
  38. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/api/client.py +0 -0
  39. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/app/__init__.py +0 -0
  40. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/app/app.py +0 -0
  41. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/app/search_apps.py +0 -0
  42. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_api_client/__init__.py +0 -0
  43. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_api_client/api_client.py +0 -0
  44. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_api_client/auth.py +0 -0
  45. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_api_client/biolib_app_api.py +0 -0
  46. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_api_client/biolib_job_api.py +0 -0
  47. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_api_client/common_types.py +0 -0
  48. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_api_client/job_types.py +0 -0
  49. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_api_client/lfs_types.py +0 -0
  50. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_api_client/user_state.py +0 -0
  51. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/__init__.py +0 -0
  52. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/base_bbf_package.py +0 -0
  53. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/file_in_container.py +0 -0
  54. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/module_input.py +0 -0
  55. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/module_output_v2.py +0 -0
  56. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/remote_endpoints.py +0 -0
  57. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/remote_stream_seeker.py +0 -0
  58. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/saved_job.py +0 -0
  59. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/stdout_and_stderr.py +0 -0
  60. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/system_exception.py +0 -0
  61. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/system_status_update.py +0 -0
  62. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_binary_format/utils.py +0 -0
  63. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_docker_client/__init__.py +0 -0
  64. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_download_container.py +0 -0
  65. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_errors.py +0 -0
  66. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/biolib_logging.py +0 -0
  67. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/cli/__init__.py +0 -0
  68. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/cli/auth.py +0 -0
  69. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/cli/download_container.py +0 -0
  70. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/cli/init.py +0 -0
  71. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/cli/lfs.py +0 -0
  72. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/cli/push.py +0 -0
  73. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/cli/run.py +0 -0
  74. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/cli/runtime.py +0 -0
  75. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/cli/start.py +0 -0
  76. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/.gitignore +0 -0
  77. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/__init__.py +0 -0
  78. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/cloud_utils/__init__.py +0 -0
  79. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/cloud_utils/cloud_utils.py +0 -0
  80. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/__init__.py +0 -0
  81. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/cache_state.py +0 -0
  82. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/cache_types.py +0 -0
  83. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/docker_image_cache.py +0 -0
  84. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/executors/__init__.py +0 -0
  85. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/executors/docker_types.py +0 -0
  86. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/executors/tars/__init__.py +0 -0
  87. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/executors/types.py +0 -0
  88. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/job_legacy_input_wait_timeout_thread.py +0 -0
  89. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/job_max_runtime_timer_thread.py +0 -0
  90. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/job_worker.py +0 -0
  91. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/large_file_system.py +0 -0
  92. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/mappings.py +0 -0
  93. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/utilization_reporter_thread.py +0 -0
  94. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/job_worker/utils.py +0 -0
  95. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/socker_listener_thread.py +0 -0
  96. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/socket_sender_thread.py +0 -0
  97. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/utils.py +0 -0
  98. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/webserver/__init__.py +0 -0
  99. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/webserver/gunicorn_flask_application.py +0 -0
  100. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/webserver/webserver.py +0 -0
  101. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/webserver/webserver_types.py +0 -0
  102. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/compute_node/webserver/webserver_utils.py +0 -0
  103. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/experiments/__init__.py +0 -0
  104. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/experiments/experiment.py +0 -0
  105. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/jobs/__init__.py +0 -0
  106. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/jobs/job.py +0 -0
  107. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/jobs/job_result.py +0 -0
  108. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/jobs/types.py +0 -0
  109. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/runtime/__init__.py +0 -0
  110. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/sdk/__init__.py +0 -0
  111. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/tables.py +0 -0
  112. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/templates/__init__.py +0 -0
  113. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/templates/example_app.py +0 -0
  114. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/typing_utils.py +0 -0
  115. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/user/__init__.py +0 -0
  116. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/user/sign_in.py +0 -0
  117. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/utils/__init__.py +0 -0
  118. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/utils/app_uri.py +0 -0
  119. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/utils/cache_state.py +0 -0
  120. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/utils/multipart_uploader.py +0 -0
  121. {pybiolib-1.1.2189 → pybiolib-1.2.12}/biolib/utils/zip/remote_zip.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pybiolib
3
- Version: 1.1.2189
3
+ Version: 1.2.12
4
4
  Summary: BioLib Python Client
5
5
  Home-page: https://github.com/biolib
6
6
  License: MIT
@@ -83,6 +83,8 @@ def verify_schema(specification: SqliteV1DatabaseSchema, actual_schema: SqliteV1
83
83
 
84
84
  def get_data_record_state_from_uri(uri) -> 'DataRecordVersionInfo':
85
85
  app_response: AppGetResponse = api_client.get(path='/app/', params={'uri': uri}).json()
86
+ if app_response['app']['type'] != 'data-record':
87
+ raise Exception(f'Resource "{uri}" is not a Data Record')
86
88
  return DataRecordVersionInfo(
87
89
  resource_uri=app_response['app_version']['app_uri'],
88
90
  resource_uuid=app_response['app']['public_id'],
@@ -125,12 +125,14 @@ class HttpClient:
125
125
 
126
126
  except urllib.error.URLError as error:
127
127
  if isinstance(error.reason, socket.timeout):
128
- logger_no_user_data.warning(f'HTTP {method} request failed with read timeout for "{url}"')
128
+ if retry_count > 0:
129
+ logger_no_user_data.warning(f'HTTP {method} request failed with read timeout for "{url}"')
129
130
  last_error = error
130
131
  else:
131
132
  raise error
132
133
  except socket.timeout as error:
133
- logger_no_user_data.warning(f'HTTP {method} request failed with read timeout for "{url}"')
134
+ if retry_count > 0:
135
+ logger_no_user_data.warning(f'HTTP {method} request failed with read timeout for "{url}"')
134
136
  last_error = error
135
137
 
136
138
  raise last_error or Exception(f'HTTP {method} request failed after {retries} retries for "{url}"')
@@ -211,6 +211,11 @@ def push_application(
211
211
  try:
212
212
  logger.info(f'Trying to push image {docker_image_name} defined on module {module_name}.')
213
213
  image = docker_client.images.get(docker_image_name)
214
+ architecture = image.attrs.get('Architecture')
215
+ if architecture != 'amd64':
216
+ print(f"Error: '{docker_image_name}' is compiled for {architecture}, expected x86 (amd64).")
217
+ print('If you are on an ARM processor, try passing --platform linux/amd64 to docker build.')
218
+ exit(1)
214
219
  absolute_repo_uri = f'{utils.BIOLIB_SITE_HOSTNAME}/{repo}'
215
220
  image.tag(absolute_repo_uri, tag)
216
221
 
@@ -7,6 +7,7 @@ class RuntimeJobDataDict(TypedDict):
7
7
  job_uuid: str
8
8
  job_auth_token: str
9
9
  app_uri: str
10
+ is_environment_biolib_cloud: bool
10
11
 
11
12
 
12
13
  class BioLibRuntimeError(Exception):
@@ -0,0 +1,264 @@
1
+ import glob
2
+ import os
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ import tempfile
7
+
8
+ import biolib
9
+ from biolib.utils import SeqUtil
10
+
11
+
12
+ def natsorted(lst):
13
+ """Sort the list using the natural sort key."""
14
+
15
+ def _natural_sort_key(s):
16
+ """A key function for natural sorting."""
17
+ return [int(text) if text.isdigit() else text.lower() for text in re.split('([0-9]+)', s)]
18
+
19
+ return sorted(lst, key=_natural_sort_key)
20
+
21
+
22
+ def fasta_above_threshold(fasta_file, work_threshold, work_per_residue=1, verbose=False):
23
+ """True if total FASYA residue work above max_work"""
24
+
25
+ records = SeqUtil.parse_fasta(fasta_file)
26
+
27
+ # Calculate work units
28
+ total_work_units = 0
29
+ for i, record in enumerate(records):
30
+ sequence_work_units = len(record.sequence) * work_per_residue
31
+ total_work_units += sequence_work_units
32
+
33
+ if total_work_units >= work_threshold:
34
+ if verbose:
35
+ print(f'FASTA above threshold (stopped at {total_work_units}) >= {work_threshold}')
36
+ print(f'From from {i+1}/{len(records)} sequences in {fasta_file}')
37
+ return True
38
+
39
+ if verbose:
40
+ print(f'FASTA below threshold ({total_work_units}) < {work_threshold}')
41
+ print(f'From {len(records)} sequences in {fasta_file}')
42
+
43
+ return False
44
+
45
+
46
+ def run_locally(command_list, args):
47
+ """Run script locally (no multi-node processing)"""
48
+
49
+ # Prepare command
50
+ new_args = vars(args)
51
+
52
+ # Delete multinode-specific input arguments
53
+ for k in list(new_args.keys()):
54
+ if str(k).startswith('multinode'):
55
+ del new_args[k]
56
+
57
+ # Convert to list format
58
+ new_args_list = _args_dict_to_args_list(new_args)
59
+
60
+ # Prepare command, e.g. ["python3", "predict.py"] + new_args_list
61
+ command = command_list + new_args_list
62
+
63
+ if args.verbose >= 1:
64
+ print(f'Running {command}')
65
+
66
+ # Run command
67
+ result = subprocess.run(command, capture_output=True, text=True, check=False)
68
+ if result.returncode == 0:
69
+ print(f'{result.stdout}')
70
+ else:
71
+ print(f'Error: {result.stderr}')
72
+
73
+
74
+ def fasta_batch_records(fasta_file, work_per_batch_min, work_per_residue=1, verbose=False):
75
+ """Converts FASTA records to batches of records, based on thresholds"""
76
+
77
+ def log_batches(batches):
78
+ for i, batch in enumerate(batches):
79
+ batch_dict = {
80
+ 'records': len(batch),
81
+ 'residues': sum(len(record.sequence) for record in batch),
82
+ }
83
+
84
+ n_seqs, n_res = batch_dict['records'], batch_dict['residues']
85
+ print(f'Batch {i+1}: {n_res} residues from {n_seqs} sequences')
86
+
87
+ records = SeqUtil.parse_fasta(fasta_file)
88
+
89
+ batches = []
90
+ batch = []
91
+ current_work_units = 0
92
+ total_work_units = 0
93
+ for record in records:
94
+ # Add to batch
95
+ batch.append(record)
96
+
97
+ # Calculate work units
98
+ seq = record.sequence
99
+ sequence_work_units = len(seq) * work_per_residue
100
+
101
+ # Increase counters
102
+ current_work_units += sequence_work_units
103
+ total_work_units += sequence_work_units
104
+
105
+ # If above limit, start a new batch
106
+ if current_work_units >= work_per_batch_min:
107
+ batches.append(batch)
108
+ batch = []
109
+ current_work_units = 0
110
+
111
+ # Append last batch if present
112
+ if batch:
113
+ batches.append(batch)
114
+
115
+ if verbose:
116
+ log_batches(batches)
117
+
118
+ return batches
119
+
120
+
121
+ def fasta_send_batches_biolib(app_url, batches, args, args_fasta='fasta', verbose=1):
122
+ """
123
+ Send jobs through pybiolib interface
124
+ """
125
+
126
+ if args.verbose >= 1:
127
+ print(f'Sending {len(batches)} batches to Biolib')
128
+
129
+ # Login to biolib, prepare app
130
+ # current_app = biolib.load(Runtime.get_app_uri())
131
+ biolib.login()
132
+ current_app = biolib.load(app_url) # Nb: uses "_" not "-"
133
+
134
+ # Compute results
135
+ job_list = []
136
+ for i, batch_records in enumerate(batches): # MH
137
+ # Write FASTA, send to server
138
+ with tempfile.TemporaryDirectory() as tempdir:
139
+ # New arguments
140
+ new_args = vars(args)
141
+
142
+ # Write batched FASTA to send
143
+ fasta_path = f'{tempdir}/input.fasta'
144
+ SeqUtil.write_records_to_fasta(fasta_path, batch_records)
145
+ new_args[args_fasta] = fasta_path
146
+ new_args['multinode_only_local'] = True
147
+
148
+ # Convert to list
149
+ new_args_list = _args_dict_to_args_list(new_args)
150
+
151
+ # Send job
152
+ job = current_app.cli(args=new_args_list, blocking=False)
153
+ job_list.append(job)
154
+
155
+ # Job stats
156
+ if args.verbose:
157
+ batch_dict = _get_batch_stats(batch_records)
158
+ n_seqs, n_res = batch_dict['records'], batch_dict['residues']
159
+ print(f'Sending job {i+1}: {n_res} residues from {n_seqs} sequences -> arg_list = {new_args_list}')
160
+
161
+ # Stream job output at a time
162
+ print('Streaming job outputs ...')
163
+ for i, job in enumerate(job_list):
164
+ job.stream_logs()
165
+
166
+ # Check if job succeeded
167
+ assert job.get_exit_code() == 0, f'Job failed with exit code {job.get_exit_code()}'
168
+
169
+ # Write to disk
170
+ output_dir = f'job_output/job_{i+1}'
171
+ job.save_files(output_dir=output_dir)
172
+
173
+ if verbose:
174
+ print(f'Saving to {output_dir}')
175
+
176
+
177
+ def merge_folder(folder_name, job_out_dir='job_output', out_dir='output', verbose=1):
178
+ """Helper function for merging folders"""
179
+
180
+ os.makedirs(out_dir, exist_ok=True)
181
+
182
+ job_dirs = glob.glob(f'{job_out_dir}/job_*')
183
+ job_dirs = natsorted(job_dirs)
184
+
185
+ # Move first file, prepare to merge
186
+ first_folder = f'{job_dirs[0]}/{folder_name}'
187
+ merged_folder = f'{out_dir}/{folder_name}'
188
+ shutil.move(first_folder, merged_folder)
189
+
190
+ if verbose:
191
+ print(f'Merging {folder_name} from {len(job_dirs)} directories to {merged_folder}')
192
+
193
+ # If more than one folder, merge to first
194
+ if len(job_dirs) >= 2:
195
+ # Find each job output file
196
+ for job_dir in job_dirs[1:]:
197
+ # Move over extra files
198
+ extra_folder = f'{job_dir}/{folder_name}'
199
+ extra_files = os.listdir(extra_folder)
200
+ for file_name in extra_files:
201
+ file_path = f'{extra_folder}/{file_name}'
202
+ shutil.move(file_path, merged_folder)
203
+
204
+
205
+ def merge_file(
206
+ file_name,
207
+ header_lines_int=1,
208
+ job_out_dir='job_output',
209
+ out_dir='output',
210
+ verbose=1,
211
+ ):
212
+ """Helper function for merging files with headers"""
213
+
214
+ os.makedirs(out_dir, exist_ok=True)
215
+
216
+ job_dirs = glob.glob(f'{job_out_dir}/job_*')
217
+ job_dirs = natsorted(job_dirs)
218
+
219
+ # Move first file, prepare to merge
220
+ first_file = f'{job_dirs[0]}/{file_name}'
221
+ merged_file = f'{out_dir}/{file_name}'
222
+ shutil.move(first_file, merged_file)
223
+
224
+ if verbose:
225
+ print(f'Merging {file_name} from {len(job_dirs)} directories to {merged_file}')
226
+
227
+ # If more than one file, append to first
228
+ if len(job_dirs) >= 2:
229
+ # Open first file
230
+ with open(merged_file, 'a') as merged_file_handle:
231
+ # Find each job output file
232
+ for job_dir in job_dirs[1:]:
233
+ # Open extra file
234
+ extra_file = f'{job_dir}/{file_name}'
235
+ with open(extra_file) as extra_file_handle:
236
+ # Skip first n header lines
237
+ for _ in range(header_lines_int):
238
+ next(extra_file_handle)
239
+
240
+ # Append content to first file
241
+ contents = extra_file_handle.read()
242
+ merged_file_handle.write(contents)
243
+
244
+
245
+ def _get_batch_stats(batch):
246
+ stats_dict = {
247
+ 'records': len(batch),
248
+ 'residues': sum(len(R.sequence) for R in batch),
249
+ }
250
+
251
+ return stats_dict
252
+
253
+
254
+ def _args_dict_to_args_list(new_args):
255
+ """Converts args dict to list of arguments for Biolib"""
256
+
257
+ nested_list = [[f'--{key}', f'{value}'] for key, value in new_args.items()]
258
+
259
+ arg_list = []
260
+ for lst in nested_list:
261
+ for item in lst:
262
+ arg_list.append(item)
263
+
264
+ return arg_list
@@ -14,6 +14,10 @@ class Runtime:
14
14
  def check_is_environment_biolib_app() -> bool:
15
15
  return bool(Runtime._try_to_get_job_data())
16
16
 
17
+ @staticmethod
18
+ def check_is_environment_biolib_cloud() -> bool:
19
+ return Runtime._get_job_data().get('is_environment_biolib_cloud', False)
20
+
17
21
  @staticmethod
18
22
  def get_job_id() -> str:
19
23
  return Runtime._get_job_data()['job_uuid']
@@ -32,6 +32,7 @@ class App(TypedDict):
32
32
  public_id: str
33
33
  state: str
34
34
  resource_uri: str
35
+ type: str
35
36
 
36
37
 
37
38
  class AppGetResponse(TypedDict):
@@ -6,6 +6,7 @@ from typing import Dict, List
6
6
  import click
7
7
 
8
8
  from biolib._data_record.data_record import DataRecord
9
+ from biolib.biolib_api_client import BiolibApiClient
9
10
  from biolib.biolib_logging import logger, logger_no_user_data
10
11
  from biolib.typing_utils import Optional
11
12
 
@@ -57,6 +58,7 @@ def download(uri: str, file: Optional[str], path_filter: Optional[str]) -> None:
57
58
  @click.argument('uri', required=True)
58
59
  @click.option('--json', 'output_as_json', is_flag=True, default=False, required=False, help='Format output as JSON')
59
60
  def describe(uri: str, output_as_json: bool) -> None:
61
+ BiolibApiClient.assert_is_signed_in(authenticated_action_description='get Data Record description')
60
62
  record = DataRecord.get_by_uri(uri)
61
63
  files_info: List[Dict] = []
62
64
  total_size_in_bytes = 0
@@ -286,6 +286,7 @@ class DockerExecutor:
286
286
  job_uuid=self._options['job']['public_id'],
287
287
  job_auth_token=self._options['job']['auth_token'],
288
288
  app_uri=self._options['job']['app_uri'],
289
+ is_environment_biolib_cloud=bool(utils.IS_RUNNING_IN_CLOUD),
289
290
  )
290
291
  secrets: Dict[str, str] = dict(
291
292
  **module.get('secrets', {}),
@@ -47,8 +47,21 @@ class JobStorage:
47
47
  module_output_path = os.path.join(job_temporary_dir, JobStorage.module_output_file_name)
48
48
  module_output_size = os.path.getsize(module_output_path)
49
49
 
50
+ # Calculate chunk size based on max chunk count of 10_000, using 9_000 to be on the safe side
51
+ max_chunk_count = 9_000
52
+ min_chunk_size_bytes = 50_000_000
53
+ chunk_size_in_bytes = max(min_chunk_size_bytes, module_output_size // max_chunk_count)
54
+
55
+ logger_no_user_data.debug(
56
+ f'Job "{job_uuid}" uploading result of size {module_output_size} bytes '
57
+ f'with chunk size of {chunk_size_in_bytes} bytes...'
58
+ )
59
+
50
60
  with open(module_output_path, mode='rb') as module_output_file:
51
- module_output_iterator = get_chunk_iterator_from_file_object(module_output_file)
61
+ module_output_iterator = get_chunk_iterator_from_file_object(
62
+ file_object=module_output_file,
63
+ chunk_size_in_bytes=chunk_size_in_bytes,
64
+ )
52
65
  multipart_uploader = JobStorage._get_module_output_uploader(job_uuid)
53
66
  multipart_uploader.upload(
54
67
  payload_iterator=module_output_iterator,
@@ -1,7 +1,9 @@
1
+ import base64
1
2
  import io
2
3
  import subprocess
3
4
  import tarfile
4
5
  import time
6
+ from urllib.parse import urlparse
5
7
 
6
8
  from docker.errors import ImageNotFound # type: ignore
7
9
  from docker.models.containers import Container # type: ignore
@@ -145,14 +147,12 @@ class RemoteHostProxy:
145
147
  raise Exception('RemoteHostProxy container not defined when attempting to write NGINX config')
146
148
 
147
149
  docker = BiolibDockerClient.get_docker_client()
148
- base_url = BiolibApiClient.get().base_url
150
+ upstream_hostname = urlparse(BiolibApiClient.get().base_url).hostname
149
151
  if self.is_app_caller_proxy:
150
- if not utils.IS_RUNNING_IN_CLOUD or not utils.BIOLIB_CLOUD_BASE_URL:
152
+ if not utils.IS_RUNNING_IN_CLOUD:
151
153
  raise BioLibError('Calling apps inside apps is not supported in local compute environment')
152
154
 
153
155
  logger_no_user_data.debug(f'Job "{self._job_uuid}" writing config for and starting App Caller Proxy...')
154
- cloud_base_url = utils.BIOLIB_CLOUD_BASE_URL
155
-
156
156
  config = CloudUtils.get_webserver_config()
157
157
  compute_node_uuid = config['compute_node_info']['public_id']
158
158
  compute_node_auth_token = config['compute_node_info']['auth_token']
@@ -161,6 +161,9 @@ class RemoteHostProxy:
161
161
  access_token = BiolibApiClient.get().access_token
162
162
  bearer_token = f'Bearer {access_token}' if access_token else ''
163
163
 
164
+ biolib_index_basic_auth = f'compute_node|admin:{compute_node_auth_token},{self._job_uuid}'
165
+ biolib_index_basic_auth_base64 = base64.b64encode(biolib_index_basic_auth.encode('utf-8')).decode('utf-8')
166
+
164
167
  nginx_config = f"""
165
168
  events {{
166
169
  worker_connections 1024;
@@ -186,10 +189,11 @@ http {{
186
189
 
187
190
  server {{
188
191
  listen 80;
189
- resolver 127.0.0.11 valid=30s;
192
+ resolver 127.0.0.11 ipv6=off valid=30s;
193
+ set $upstream_hostname {upstream_hostname};
190
194
 
191
195
  location ~* "^/api/jobs/cloud/(?<job_id>[a-z0-9-]{{36}})/status/$" {{
192
- proxy_pass {base_url}/api/jobs/cloud/$job_id/status/;
196
+ proxy_pass https://$upstream_hostname/api/jobs/cloud/$job_id/status/;
193
197
  proxy_set_header authorization $bearer_token_on_get;
194
198
  proxy_set_header cookie "";
195
199
  proxy_ssl_server_name on;
@@ -197,35 +201,35 @@ http {{
197
201
 
198
202
  location ~* "^/api/jobs/cloud/$" {{
199
203
  # Note: Using $1 here as URI part from regex must be used for proxy_pass
200
- proxy_pass {base_url}/api/jobs/cloud/$1;
204
+ proxy_pass https://$upstream_hostname/api/jobs/cloud/$1;
201
205
  proxy_set_header authorization $bearer_token_on_post;
202
206
  proxy_set_header cookie "";
203
207
  proxy_ssl_server_name on;
204
208
  }}
205
209
 
206
210
  location ~* "^/api/jobs/(?<job_id>[a-z0-9-]{{36}})/storage/input/start_upload/$" {{
207
- proxy_pass {base_url}/api/jobs/$job_id/storage/input/start_upload/;
211
+ proxy_pass https://$upstream_hostname/api/jobs/$job_id/storage/input/start_upload/;
208
212
  proxy_set_header authorization "";
209
213
  proxy_set_header cookie "";
210
214
  proxy_ssl_server_name on;
211
215
  }}
212
216
 
213
217
  location ~* "^/api/jobs/(?<job_id>[a-z0-9-]{{36}})/storage/input/presigned_upload_url/$" {{
214
- proxy_pass {base_url}/api/jobs/$job_id/storage/input/presigned_upload_url/$is_args$args;
218
+ proxy_pass https://$upstream_hostname/api/jobs/$job_id/storage/input/presigned_upload_url/$is_args$args;
215
219
  proxy_set_header authorization "";
216
220
  proxy_set_header cookie "";
217
221
  proxy_ssl_server_name on;
218
222
  }}
219
223
 
220
224
  location ~* "^/api/jobs/(?<job_id>[a-z0-9-]{{36}})/storage/input/complete_upload/$" {{
221
- proxy_pass {base_url}/api/jobs/$job_id/storage/input/complete_upload/;
225
+ proxy_pass https://$upstream_hostname/api/jobs/$job_id/storage/input/complete_upload/;
222
226
  proxy_set_header authorization "";
223
227
  proxy_set_header cookie "";
224
228
  proxy_ssl_server_name on;
225
229
  }}
226
230
 
227
231
  location ~* "^/api/jobs/(?<job_id>[a-z0-9-]{{36}})/main_result/$" {{
228
- proxy_pass {base_url}/api/jobs/$job_id/main_result/;
232
+ proxy_pass https://$upstream_hostname/api/jobs/$job_id/main_result/;
229
233
  proxy_set_header authorization "";
230
234
  proxy_set_header cookie "";
231
235
  proxy_pass_request_headers on;
@@ -233,7 +237,7 @@ http {{
233
237
  }}
234
238
 
235
239
  location ~* "^/api/jobs/(?<job_id>[a-z0-9-]{{36}})/$" {{
236
- proxy_pass {base_url}/api/jobs/$job_id/;
240
+ proxy_pass https://$upstream_hostname/api/jobs/$job_id/;
237
241
  proxy_set_header authorization $bearer_token_on_patch_and_get;
238
242
  proxy_set_header caller-job-uuid "{self._job_uuid}";
239
243
  proxy_set_header cookie "";
@@ -242,7 +246,7 @@ http {{
242
246
 
243
247
  location ~* "^/api/jobs/create_job_with_data/$" {{
244
248
  # Note: Using $1 here as URI part from regex must be used for proxy_pass
245
- proxy_pass {base_url}/api/jobs/create_job_with_data/$1;
249
+ proxy_pass https://$upstream_hostname/api/jobs/create_job_with_data/$1;
246
250
  proxy_set_header authorization $bearer_token_on_post;
247
251
  proxy_set_header caller-job-uuid "{self._job_uuid}";
248
252
  proxy_set_header cookie "";
@@ -251,7 +255,7 @@ http {{
251
255
 
252
256
  location ~* "^/api/jobs/$" {{
253
257
  # Note: Using $1 here as URI part from regex must be used for proxy_pass
254
- proxy_pass {base_url}/api/jobs/$1;
258
+ proxy_pass https://$upstream_hostname/api/jobs/$1;
255
259
  proxy_set_header authorization $bearer_token_on_post;
256
260
  proxy_set_header caller-job-uuid "{self._job_uuid}";
257
261
  proxy_set_header cookie "";
@@ -260,7 +264,7 @@ http {{
260
264
 
261
265
  location ~ "^/api/jobs/{self._job_uuid}/notes/$" {{
262
266
  # Note: Using $1 here as URI part from regex must be used for proxy_pass
263
- proxy_pass {base_url}/api/jobs/{self._job_uuid}/notes/$1;
267
+ proxy_pass https://$upstream_hostname/api/jobs/{self._job_uuid}/notes/$1;
264
268
  proxy_set_header authorization "";
265
269
  proxy_set_header job-auth-token "";
266
270
  proxy_set_header compute-node-auth-token "{compute_node_auth_token}";
@@ -270,7 +274,7 @@ http {{
270
274
  }}
271
275
 
272
276
  location /api/lfs/ {{
273
- proxy_pass {base_url}/api/lfs/;
277
+ proxy_pass https://$upstream_hostname$request_uri;
274
278
  proxy_set_header authorization "";
275
279
  proxy_set_header compute-node-auth-token "{compute_node_auth_token}";
276
280
  proxy_set_header job-uuid "{self._job_uuid}";
@@ -279,7 +283,7 @@ http {{
279
283
  }}
280
284
 
281
285
  location /api/app/ {{
282
- proxy_pass {base_url}/api/app/;
286
+ proxy_pass https://$upstream_hostname$request_uri;
283
287
  proxy_set_header authorization "";
284
288
  proxy_set_header compute-node-auth-token "{compute_node_auth_token}";
285
289
  proxy_set_header job-uuid "{self._job_uuid}";
@@ -288,33 +292,40 @@ http {{
288
292
  }}
289
293
 
290
294
  location /api/ {{
291
- proxy_pass {base_url}/api/;
295
+ proxy_pass https://$upstream_hostname$request_uri;
292
296
  proxy_set_header authorization "";
293
297
  proxy_set_header cookie "";
294
298
  proxy_ssl_server_name on;
295
299
  }}
296
300
 
297
301
  location /proxy/storage/job-storage/ {{
298
- proxy_pass {cloud_base_url}/proxy/storage/job-storage/;
302
+ proxy_pass https://$upstream_hostname$request_uri;
299
303
  proxy_set_header authorization "";
300
304
  proxy_set_header cookie "";
301
305
  proxy_ssl_server_name on;
302
306
  }}
303
307
 
304
308
  location /proxy/storage/lfs/versions/ {{
305
- proxy_pass {cloud_base_url}/proxy/storage/lfs/versions/;
309
+ proxy_pass https://$upstream_hostname$request_uri;
306
310
  proxy_set_header authorization "";
307
311
  proxy_set_header cookie "";
308
312
  proxy_ssl_server_name on;
309
313
  }}
310
314
 
311
315
  location /proxy/cloud/ {{
312
- proxy_pass {cloud_base_url}/proxy/cloud/;
316
+ proxy_pass https://$upstream_hostname$request_uri;
313
317
  proxy_set_header authorization "";
314
318
  proxy_set_header cookie "";
315
319
  proxy_ssl_server_name on;
316
320
  }}
317
321
 
322
+ location /proxy/index/ {{
323
+ proxy_pass https://$upstream_hostname$request_uri;
324
+ proxy_set_header authorization "Basic {biolib_index_basic_auth_base64}";
325
+ proxy_set_header cookie "";
326
+ proxy_ssl_server_name on;
327
+ }}
328
+
318
329
  location / {{
319
330
  return 404 "Not found";
320
331
  }}
@@ -2,23 +2,23 @@ import base64
2
2
  import os
3
3
  import random
4
4
  import shutil
5
+ import socket
5
6
  import sys
6
- import time
7
7
  import threading
8
- import socket
8
+ import time
9
9
  from queue import Queue
10
10
 
11
- from biolib import utils
11
+ from biolib import api, utils
12
+ from biolib.biolib_binary_format import ModuleOutputV2, SystemException, SystemStatusUpdate
12
13
  from biolib.biolib_binary_format.utils import LocalFileIndexableBuffer
14
+ from biolib.biolib_logging import logger, logger_no_user_data
13
15
  from biolib.compute_node.cloud_utils import CloudUtils
14
16
  from biolib.compute_node.job_worker import JobWorkerProcess
15
17
  from biolib.compute_node.job_worker.job_storage import JobStorage
16
18
  from biolib.compute_node.socker_listener_thread import SocketListenerThread
17
19
  from biolib.compute_node.socket_sender_thread import SocketSenderThread
20
+ from biolib.compute_node.utils import SystemExceptionCodes, WorkerThreadException, get_package_type
18
21
  from biolib.compute_node.webserver import webserver_utils
19
- from biolib.biolib_binary_format import SystemStatusUpdate, SystemException, ModuleOutputV2
20
- from biolib.compute_node.utils import get_package_type, WorkerThreadException, SystemExceptionCodes
21
- from biolib.biolib_logging import logger, logger_no_user_data
22
22
 
23
23
  SOCKET_HOST = '127.0.0.1'
24
24
 
@@ -37,7 +37,7 @@ class WorkerThread(threading.Thread):
37
37
  self._sender_thread = None
38
38
  self._start_and_connect_to_compute_process()
39
39
 
40
- logger.debug(f"WorkerThread connected to port {self._socket_port}")
40
+ logger.debug(f'WorkerThread connected to port {self._socket_port}')
41
41
 
42
42
  except Exception as exception:
43
43
  logger_no_user_data.error(exception)
@@ -79,20 +79,16 @@ class WorkerThread(threading.Thread):
79
79
  if progress == 94:
80
80
  # Get Job exit code
81
81
  try:
82
- module_output_path = os.path.join(self._job_temporary_dir,
83
- JobStorage.module_output_file_name)
84
- module_output = ModuleOutputV2(
85
- buffer=LocalFileIndexableBuffer(
86
- filename=module_output_path
87
- )
82
+ module_output_path = os.path.join(
83
+ self._job_temporary_dir,
84
+ JobStorage.module_output_file_name,
88
85
  )
86
+ module_output = ModuleOutputV2(buffer=LocalFileIndexableBuffer(filename=module_output_path))
89
87
  self.compute_state['exit_code'] = module_output.get_exit_code()
90
88
  logger_no_user_data.debug(f"Got exit code: {self.compute_state['exit_code']}")
91
89
 
92
90
  except Exception as error: # pylint: disable=broad-except
93
- logger_no_user_data.error(
94
- f'Could not get exit_code from module output due to: {error}'
95
- )
91
+ logger_no_user_data.error(f'Could not get exit_code from module output due to: {error}')
96
92
 
97
93
  if utils.IS_RUNNING_IN_CLOUD:
98
94
  JobStorage.upload_module_output(
@@ -107,7 +103,7 @@ class WorkerThread(threading.Thread):
107
103
  elif package_type == 'SystemException':
108
104
  error_code = SystemException(package).deserialize()
109
105
  self.compute_state['status']['error_code'] = error_code
110
- logger.debug("Hit error. Terminating Worker Thread and Compute Process")
106
+ logger.debug('Hit error. Terminating Worker Thread and Compute Process')
111
107
  self.compute_state['progress'] = 95
112
108
  self.terminate()
113
109
 
@@ -153,10 +149,10 @@ class WorkerThread(threading.Thread):
153
149
 
154
150
  # Starting a thread for accepting connections before starting the process that should to connect to the socket
155
151
  logger_no_user_data.debug('Starting connection thread')
156
- self._connection_thread = threading.Thread(target=self._accept_new_socket_connection, args=[
157
- received_messages_queue,
158
- messages_to_send_queue
159
- ])
152
+ self._connection_thread = threading.Thread(
153
+ target=self._accept_new_socket_connection,
154
+ args=[received_messages_queue, messages_to_send_queue],
155
+ )
160
156
  self._connection_thread.start()
161
157
  logger_no_user_data.debug('Started connection thread')
162
158
  logger_no_user_data.debug('Starting compute process')
@@ -177,6 +173,16 @@ class WorkerThread(threading.Thread):
177
173
  self._sender_thread.start()
178
174
 
179
175
  def terminate(self) -> None:
176
+ cloud_job_uuid = self.compute_state['cloud_job_id']
177
+ exit_code = self.compute_state.get('exit_code')
178
+ system_exception_code = self.compute_state['status'].get('error_code')
179
+ if utils.IS_RUNNING_IN_CLOUD:
180
+ CloudUtils.finish_cloud_job(
181
+ cloud_job_id=cloud_job_uuid,
182
+ system_exception_code=system_exception_code,
183
+ exit_code=exit_code,
184
+ )
185
+
180
186
  deregistered_due_to_error = False
181
187
  if self._job_worker_process:
182
188
  logger_no_user_data.debug(
@@ -184,7 +190,8 @@ class WorkerThread(threading.Thread):
184
190
  )
185
191
  self._job_worker_process.terminate()
186
192
 
187
- for _ in range(10):
193
+ clean_up_timeout_in_seconds = 600
194
+ for _ in range(clean_up_timeout_in_seconds):
188
195
  if self._job_worker_process.exitcode is not None:
189
196
  logger_no_user_data.debug(
190
197
  f'Job "{self._job_uuid}" worker process exitcode {self._job_worker_process.exitcode}'
@@ -196,28 +203,18 @@ class WorkerThread(threading.Thread):
196
203
 
197
204
  if self._job_worker_process.exitcode is None:
198
205
  # TODO: Figure out if more error handling is necessary here
199
- logger_no_user_data.error(f'Job {self._job_uuid} worker process did not exit within 10 seconds')
206
+ logger_no_user_data.error(
207
+ f'Job {self._job_uuid} worker process did not exit within {clean_up_timeout_in_seconds} seconds'
208
+ )
200
209
  if utils.IS_RUNNING_IN_CLOUD:
201
210
  logger_no_user_data.error('Deregistering compute node...')
202
211
  CloudUtils.deregister(error='job_cleanup_timed_out')
203
212
  deregistered_due_to_error = True
204
213
 
205
214
  # Delete result as error occurred
206
- system_exception_code = self.compute_state['status'].get('error_code')
207
215
  if system_exception_code and os.path.exists(self._job_temporary_dir):
208
216
  shutil.rmtree(self._job_temporary_dir)
209
217
 
210
- exit_code = self.compute_state.get('exit_code')
211
-
212
- if utils.IS_RUNNING_IN_CLOUD:
213
- # Get and send compute node exception code and job exit code if present
214
- logger_no_user_data.debug(f"Sending exit code {exit_code}")
215
- CloudUtils.finish_cloud_job(
216
- cloud_job_id=self.compute_state['cloud_job_id'],
217
- system_exception_code=system_exception_code,
218
- exit_code=exit_code
219
- )
220
-
221
218
  if self._socket:
222
219
  self._socket.close()
223
220
 
@@ -225,7 +222,7 @@ class WorkerThread(threading.Thread):
225
222
  self._connection.close()
226
223
 
227
224
  if self.compute_state['progress'] == 95:
228
- seconds_to_sleep = 60 # 1 minute
225
+ seconds_to_sleep = 5
229
226
  logger_no_user_data.debug(
230
227
  f'Job "{self._job_uuid}" worker thread sleeping for {seconds_to_sleep} seconds before cleaning up'
231
228
  )
@@ -234,7 +231,7 @@ class WorkerThread(threading.Thread):
234
231
 
235
232
  compute_state_dict = webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT
236
233
  if self._job_uuid in compute_state_dict:
237
- # Delete result as user has not started download within 60 seconds
234
+ # Delete result as user has not started download
238
235
  if compute_state_dict[self._job_uuid]['progress'] == 95 and os.path.exists(self._job_temporary_dir):
239
236
  shutil.rmtree(self._job_temporary_dir)
240
237
 
@@ -245,12 +242,18 @@ class WorkerThread(threading.Thread):
245
242
  f'Job "{self._job_uuid}" could not be found, maybe it has already been cleaned up'
246
243
  )
247
244
 
248
- logger_no_user_data.debug(f'Job "{self._job_uuid}" worker thread terminated')
249
-
250
245
  if utils.IS_RUNNING_IN_CLOUD:
246
+ config = CloudUtils.get_webserver_config()
247
+ logger_no_user_data.debug(f'Job "{self._job_uuid}" reporting CloudJob "{cloud_job_uuid}" as cleaned up...')
248
+ api.client.post(
249
+ path=f'/internal/compute-nodes/cloud-jobs/{cloud_job_uuid}/cleaned-up/',
250
+ headers={'Compute-Node-Auth-Token': config['compute_node_info']['auth_token']},
251
+ )
252
+
251
253
  if deregistered_due_to_error:
252
254
  CloudUtils.shutdown() # shutdown now
253
255
  else:
254
256
  webserver_utils.update_auto_shutdown_time()
255
257
 
258
+ logger_no_user_data.debug(f'Job "{self._job_uuid}" worker thread exiting...')
256
259
  sys.exit()
@@ -35,7 +35,7 @@ class SeqUtil:
35
35
  input_file: Union[str, BufferedIOBase, None] = None,
36
36
  default_header: Optional[str] = None,
37
37
  allow_any_sequence_characters: bool = False,
38
- allow_empty_sequence: bool = False,
38
+ allow_empty_sequence: bool = True,
39
39
  file_name: Optional[str] = None,
40
40
  ) -> List[SeqUtilRecord]:
41
41
  if input_file is None:
@@ -1,8 +1,8 @@
1
1
  [tool.poetry]
2
2
  name = "pybiolib"
3
- version = "1.1.2189"
3
+ version = "1.2.12"
4
4
  description = "BioLib Python Client"
5
- readme = "README.md"
5
+ readme = "PYPI_README.md"
6
6
  license = "MIT"
7
7
  homepage = "https://github.com/biolib"
8
8
  keywords = ["biolib"]
@@ -17,7 +17,7 @@ packages = [
17
17
  { include = "biolib" },
18
18
  ]
19
19
  include = [
20
- "README.md",
20
+ "PYPI_README.md",
21
21
  "LICENSE",
22
22
  ]
23
23
 
File without changes
File without changes
File without changes
File without changes
File without changes