mantatech-sdk 0.5b0.dev65__py3-none-any.whl

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 (54) hide show
  1. manta/__init__.light.py +22 -0
  2. manta/__init__.py +83 -0
  3. manta/__main__.py +21 -0
  4. manta/apis/__init__.py +7 -0
  5. manta/apis/async_user_api.py +6458 -0
  6. manta/apis/graph.py +498 -0
  7. manta/apis/module.py +316 -0
  8. manta/apis/results.py +251 -0
  9. manta/apis/swarm.py +206 -0
  10. manta/apis/user_api.py +1016 -0
  11. manta/cli/__init__.py +1 -0
  12. manta/cli/commands/__init__.py +1 -0
  13. manta/cli/commands/base_handler.py +229 -0
  14. manta/cli/commands/doc.py +192 -0
  15. manta/cli/commands/install.py +346 -0
  16. manta/cli/commands/sdk.py +9 -0
  17. manta/cli/commands/sdk_cluster.py +211 -0
  18. manta/cli/commands/sdk_config.py +347 -0
  19. manta/cli/commands/sdk_globals.py +280 -0
  20. manta/cli/commands/sdk_logs.py +174 -0
  21. manta/cli/commands/sdk_main.py +167 -0
  22. manta/cli/commands/sdk_module.py +516 -0
  23. manta/cli/commands/sdk_nodes.py +168 -0
  24. manta/cli/commands/sdk_original.py +3873 -0
  25. manta/cli/commands/sdk_results.py +265 -0
  26. manta/cli/commands/sdk_swarm.py +454 -0
  27. manta/cli/commands/sdk_user.py +234 -0
  28. manta/cli/commands/status.py +292 -0
  29. manta/cli/component_detector.py +112 -0
  30. manta/cli/config_manager.py +445 -0
  31. manta/cli/main.py +265 -0
  32. manta/cli/utils/__init__.py +27 -0
  33. manta/cli/utils/converters.py +140 -0
  34. manta/clients/cluster_management_client.py +486 -0
  35. manta/clients/local_client.py +149 -0
  36. manta/clients/module_management_client.py +217 -0
  37. manta/clients/swarm_management_client.py +562 -0
  38. manta/clients/user_management_client.py +395 -0
  39. manta/clients/world_client.py +195 -0
  40. manta/light/__init__.py +31 -0
  41. manta/light/globals.py +245 -0
  42. manta/light/local.py +407 -0
  43. manta/light/logging_config.py +39 -0
  44. manta/light/path.py +116 -0
  45. manta/light/results.py +236 -0
  46. manta/light/task.py +100 -0
  47. manta/light/utils.py +217 -0
  48. manta/light/world.py +177 -0
  49. mantatech_sdk-0.5b0.dev65.dist-info/METADATA +1039 -0
  50. mantatech_sdk-0.5b0.dev65.dist-info/RECORD +54 -0
  51. mantatech_sdk-0.5b0.dev65.dist-info/WHEEL +5 -0
  52. mantatech_sdk-0.5b0.dev65.dist-info/entry_points.txt +2 -0
  53. mantatech_sdk-0.5b0.dev65.dist-info/licenses/LICENSE +683 -0
  54. mantatech_sdk-0.5b0.dev65.dist-info/top_level.txt +1 -0
manta/apis/module.py ADDED
@@ -0,0 +1,316 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import os
5
+ import shutil
6
+ import struct
7
+ import tempfile
8
+ import zipfile
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Dict, Optional, Union
12
+
13
+ from manta_common.build.common.tasks import MaximumType
14
+ from manta_common.build.common.tasks import Module as ModuleProto
15
+ from manta_common.build.common.tasks import Scheduling as SchedulingProto
16
+ from manta_common.build.core.user_services import ModuleRequest
17
+
18
+ # Import here to avoid circular imports
19
+
20
+
21
+ def zip_module(python_program: Union[str, Path]) -> bytes:
22
+ """
23
+ Zip multiple files into an executable python module
24
+
25
+ Parameters
26
+ ----------
27
+ python_program : Union[str, Path]
28
+ Python program location
29
+
30
+ Returns
31
+ -------
32
+ bytes
33
+ Zipped binary in memory
34
+ """
35
+ buffer = io.BytesIO()
36
+ python_program = Path(python_program)
37
+
38
+ def nopycache(path: Path):
39
+ return "pycache" not in str(path)
40
+
41
+ with tempfile.TemporaryDirectory() as folder:
42
+ folder = Path(folder)
43
+ copy = shutil.copytree if python_program.is_dir() else shutil.copyfile
44
+ copy(python_program, folder / python_program.name)
45
+
46
+ # Create deterministic ZIP archive by manually setting fixed timestamps
47
+ # Fixed timestamp for deterministic ZIP creation (2023-01-01 00:00:00)
48
+ FIXED_TIMESTAMP = (2023, 1, 1, 0, 0, 0)
49
+
50
+ with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
51
+ # Add all files from the source folder
52
+ for root, dirs, files in os.walk(folder):
53
+ # Filter out pycache directories
54
+ dirs[:] = [d for d in dirs if "pycache" not in d]
55
+
56
+ for file in files:
57
+ file_path = Path(root) / file
58
+ if nopycache(file_path):
59
+ # Calculate relative path for archive
60
+ arc_path = file_path.relative_to(folder)
61
+
62
+ # Create ZipInfo with fixed timestamp
63
+ zip_info = zipfile.ZipInfo(str(arc_path))
64
+ zip_info.date_time = FIXED_TIMESTAMP
65
+ zip_info.compress_type = zipfile.ZIP_DEFLATED
66
+
67
+ # Read file content and add to archive
68
+ with open(file_path, "rb") as src_file:
69
+ zf.writestr(zip_info, src_file.read())
70
+
71
+ # Add the __main__.py file for zipapp functionality
72
+ main_content = f"""# -*- coding: utf-8 -*-
73
+ import {python_program.stem}
74
+ {python_program.stem}.main()
75
+ """
76
+ main_zip_info = zipfile.ZipInfo("__main__.py")
77
+ main_zip_info.date_time = FIXED_TIMESTAMP
78
+ main_zip_info.compress_type = zipfile.ZIP_DEFLATED
79
+ zf.writestr(main_zip_info, main_content.encode("utf-8"))
80
+
81
+ return buffer.getvalue()
82
+
83
+
84
+ @dataclass
85
+ class Module:
86
+ """
87
+ Class which holds information about a `Module`
88
+
89
+ It helps the user to indicate which Nodes must executed a :class:`Module <manta.module.Module>`.
90
+
91
+ During the execution of a :class:`Swarm <manta.swarm.Swarm>`, the Manager selects nodes
92
+ depending on attributes set in this class.
93
+
94
+ For instance, :code:`Module(method="any", excepted_ids=["node_28"], maximum=0.8)`
95
+ indicates that the module will be executed on 80% of nodes excepted :code:`"node_28"`.
96
+
97
+ Attributes
98
+ ----------
99
+ python_program : Union[str, Path]
100
+ python program code
101
+ image : str
102
+ image of the container in which the program will be executed
103
+ alias : Optional[str]
104
+ Alias task name. If specified, it is used to generate to a unique ID
105
+ network : Optional[str]
106
+ It indicates in which network the task will be executed.
107
+ Network name must exist in :code:`Swarm.networks`.
108
+ gpu : bool
109
+ If :code:`True`, the module will be executed on GPU
110
+ python_files : Dict[str, str]
111
+ Dictionary containing unzipped python files {filename: content}
112
+ """
113
+
114
+ python_program: Union[str, Path]
115
+ image: str
116
+ name: Optional[str] = field(default=None)
117
+ datasets: Optional[list] = field(default=None)
118
+ python_files: Dict[str, str] = field(default_factory=dict)
119
+ module_id: Optional[str] = field(default=None)
120
+
121
+ def __post_init__(self):
122
+ if self.name is None:
123
+ self.name = Path(self.python_program).stem
124
+
125
+ def to_proto(self):
126
+ """Convert to protobuf object
127
+
128
+ Returns
129
+ -------
130
+ Object
131
+ Protobuf object
132
+ """
133
+ return ModuleProto(
134
+ python_program=zip_module(self.python_program),
135
+ image=self.image,
136
+ name=self.name or "",
137
+ datasets=self.datasets or [],
138
+ )
139
+
140
+ def __str__(self):
141
+ return self.name
142
+
143
+ @classmethod
144
+ def from_proto(cls, proto: Union[ModuleProto, ModuleRequest]) -> "Module":
145
+ """
146
+ Create a Module from a protobuf object
147
+
148
+ Parameters
149
+ ----------
150
+ proto : Union[ModuleProto, ModuleRequest]
151
+ Protobuf module object
152
+
153
+ Returns
154
+ -------
155
+ Module
156
+ Module object with unzipped python_files
157
+ """
158
+ # Unzip the python_program bytes
159
+ python_files: Dict[str, str] = {}
160
+ if isinstance(proto, ModuleProto):
161
+ module = proto
162
+ module_id = ""
163
+ image = proto.image
164
+ name = proto.name
165
+ datasets = list(proto.datasets) if proto.datasets else None
166
+
167
+ elif isinstance(proto, ModuleRequest):
168
+ module = proto.module
169
+ module_id = proto.module_id
170
+ image = proto.module.image
171
+ name = proto.module.name
172
+ datasets = list(proto.module.datasets) if proto.module.datasets else None
173
+
174
+ try:
175
+ with zipfile.ZipFile(io.BytesIO(module.python_program), "r") as zip_file:
176
+ for file_name in zip_file.namelist():
177
+ # Skip directories and __pycache__ files
178
+ if not file_name.endswith("/") and "__pycache__" not in file_name:
179
+ with zip_file.open(file_name) as file:
180
+ python_files[file_name] = file.read().decode("utf-8")
181
+ except zipfile.BadZipFile:
182
+ # If it's not a zip file, treat as raw python code
183
+ print(
184
+ f"Warning: {module.python_program} is not a zip file, treating as raw python code"
185
+ )
186
+ python_files["main.py"] = module.python_program.decode("utf-8")
187
+
188
+ return cls(
189
+ python_program="", # Empty since we have python_files
190
+ image=image,
191
+ name=name,
192
+ datasets=datasets,
193
+ python_files=python_files,
194
+ module_id=module_id,
195
+ )
196
+
197
+ def print_files(self):
198
+ """
199
+ Print the module files in a readable format
200
+ """
201
+ if self.python_files:
202
+ print(f"=== Module: {self.name} ===")
203
+ print(f"Image: {self.image}")
204
+ if self.datasets:
205
+ print(f"Datasets: {self.datasets}")
206
+ print(f"Files ({len(self.python_files)}):")
207
+ print("-" * 50)
208
+
209
+ for filename, content in self.python_files.items():
210
+ print(f"\n📁 {filename}")
211
+ print("-" * len(filename))
212
+ # Add line numbers for better readability
213
+ lines = content.split("\n")
214
+ for i, line in enumerate(lines, 1):
215
+ print(f"{i:3d} | {line}")
216
+ print("-" * 50)
217
+ else:
218
+ # Fallback to python_program if python_files is empty
219
+ print(f"=== Module: {self.name} ===")
220
+ print(f"Image: {self.image}")
221
+ if self.datasets:
222
+ print(f"Datasets: {self.datasets}")
223
+ print(f"Python program: {self.python_program}")
224
+
225
+ # Try to read and display the file if it's a path
226
+ if isinstance(self.python_program, (str, Path)):
227
+ try:
228
+ path = Path(self.python_program)
229
+ if path.exists():
230
+ print("-" * 50)
231
+ print(f"\n📁 {path.name}")
232
+ print("-" * len(path.name))
233
+ content = path.read_text(encoding="utf-8")
234
+ lines = content.split("\n")
235
+ for i, line in enumerate(lines, 1):
236
+ print(f"{i:3d} | {line}")
237
+ print("-" * 50)
238
+ except Exception as e:
239
+ print(f"Could not read file: {e}")
240
+
241
+
242
+ @dataclass
243
+ class Scheduling:
244
+ """
245
+ Class which holds scheduling information for a `Module`
246
+
247
+ Attributes
248
+ ----------
249
+ method : str
250
+ Currently, "any" or "best_resources" or "tag-<your-tag>"
251
+ Note with tag, a result, associated to your tag, should
252
+ be available and sent by previous module
253
+ fixed : bool
254
+ if :code:`True`, the chosen nodes must remain the same
255
+ over the execution of the :class:`Swarm <manta.swarm.Swarm>`
256
+ excepted_ids : Optional[list]
257
+ List of ids that must not be chosen for this module
258
+ specified_ids : Optional[list]
259
+ List of ids that must be chosen for this module
260
+ maximum : Optional[Union[int, float]]
261
+ maximum of ids that can be chosen for this module.
262
+ If it is a float, it is a percentage between :code:`0.0` and :code:`1.0`
263
+ gpu : bool
264
+ If :code:`True`, the module will be executed on GPU
265
+ datasets : Optional[list]
266
+ List of datasets required for this module
267
+ """
268
+
269
+ method: str
270
+ fixed: bool
271
+ excepted_ids: Optional[list]
272
+ specified_ids: Optional[list]
273
+ maximum: Optional[Union[int, float]]
274
+ gpu: bool
275
+
276
+ def __post_init__(self):
277
+ if self.specified_ids is not None:
278
+ if self.excepted_ids is not None and not set(self.excepted_ids) & set(
279
+ self.specified_ids
280
+ ):
281
+ raise ValueError("No coherence between specified ids and excepted ids")
282
+ offset = 0 if self.excepted_ids is None else len(self.excepted_ids)
283
+ if isinstance(self.maximum, int):
284
+ # Regulate maximum if maximum is over estimated
285
+ self.maximum = min(len(self.specified_ids) - offset, self.maximum)
286
+
287
+ def to_proto(self):
288
+ """Convert to protobuf object
289
+
290
+ Returns
291
+ -------
292
+ Object
293
+ Protobuf object
294
+ """
295
+ # Serialize maximum value
296
+ if isinstance(self.maximum, float):
297
+ maximum = struct.pack("f", self.maximum)
298
+ max_type = MaximumType.FLOAT
299
+ elif isinstance(self.maximum, int):
300
+ maximum = struct.pack("i", self.maximum)
301
+ max_type = MaximumType.INT
302
+ elif self.maximum is None:
303
+ maximum = struct.pack("i", 0)
304
+ max_type = MaximumType.INT
305
+ else:
306
+ raise ValueError("Maximum must be a float or an int")
307
+
308
+ return SchedulingProto(
309
+ method=self.method,
310
+ fixed=self.fixed,
311
+ excepted_ids=self.excepted_ids or [],
312
+ specified_ids=self.specified_ids or [],
313
+ maximum=maximum,
314
+ max_type=max_type,
315
+ gpu=self.gpu,
316
+ )
manta/apis/results.py ADDED
@@ -0,0 +1,251 @@
1
+ from functools import reduce
2
+ from operator import ior, itemgetter
3
+ from typing import Any, Dict, List, Tuple, Union
4
+
5
+ __all__ = ["Results"]
6
+
7
+
8
+ def is_int(obj: Any) -> bool:
9
+ """
10
+ Check if the :code:`obj` is an integer
11
+
12
+ Parameters
13
+ ----------
14
+ obj : Any
15
+ Object input
16
+
17
+ Returns
18
+ -------
19
+ bool
20
+ Is an integer
21
+ """
22
+ return isinstance(obj, int)
23
+
24
+
25
+ class Schema:
26
+ """
27
+ This class represents the rows and columns of values from
28
+ results.
29
+
30
+ Parameters
31
+ ----------
32
+ values : List[Dict[str, Dict[str, Any]]]
33
+ List of values from results
34
+
35
+ Attributes
36
+ ----------
37
+ unordered_rows : Set[str]
38
+ Node IDs
39
+ unordered_columns : Set[str]
40
+ Tags
41
+ """
42
+
43
+ __slots__ = ["unordered_rows", "unordered_columns"]
44
+
45
+ def __init__(self, values: List[Dict[str, Dict[str, Any]]]):
46
+ def ior_dict(dictn, next_item):
47
+ return ior(dictn, next_item.keys())
48
+
49
+ def reduce_ior_dict(dictn, next_item):
50
+ return reduce(ior_dict, next_item.values(), dictn)
51
+
52
+ self.unordered_rows = reduce(ior_dict, values, set())
53
+ self.unordered_columns = reduce(reduce_ior_dict, values, set())
54
+
55
+ def parse(self, values: Dict[str, Dict[str, Any]]) -> List[List[Any]]:
56
+ """
57
+ Order results based on the schema values
58
+
59
+ Parameters
60
+ ----------
61
+ values : Dict[str, Dict[str, Any]]
62
+ Input values for one iteration
63
+
64
+ Returns
65
+ -------
66
+ List[List[Any]]
67
+ Ordered values
68
+ """
69
+ return [
70
+ [values.get(row, {}).get(column, None) for column in self.columns]
71
+ for row in self.rows
72
+ ]
73
+
74
+ @property
75
+ def rows(self) -> List[str]:
76
+ """
77
+ Convenient to have sorted rows
78
+
79
+ Returns
80
+ -------
81
+ List[str]
82
+ Sorted rows
83
+ """
84
+ return sorted(self.unordered_rows)
85
+
86
+ @property
87
+ def columns(self) -> List[str]:
88
+ """
89
+ Convenient to have sorted columns
90
+
91
+ Returns
92
+ -------
93
+ List[str]
94
+ Sorted columns
95
+ """
96
+ return sorted(self.unordered_columns)
97
+
98
+ def __str__(self): # pragma: no cover
99
+ return f"Schema(rows={self.rows}, columns={self.columns})"
100
+
101
+ def __repr__(self): # pragma: no cover
102
+ return str(self)
103
+
104
+
105
+ class Results:
106
+ """
107
+ This class organizes the results gotten from the database
108
+ for ergonomic manipulations
109
+
110
+ Parameters
111
+ ----------
112
+ values : List[Dict[str, Dict[str, Any]]]
113
+ Values organized by the class
114
+
115
+ Examples
116
+ --------
117
+
118
+ >>> values = [
119
+ ... {
120
+ ... "node_c": {"accuracy": 0.8, "params": [0.80, 0.84]},
121
+ ... "node_a": {"accuracy": 0.7, "params": [0.61, 0.72]},
122
+ ... "node_b": {"accuracy": 0.1, "params": [0.23, 0.37]},
123
+ ... },
124
+ ... {
125
+ ... "node_c": {"accuracy": 0.1, "params": [0.53, 0.68]},
126
+ ... "node_a": {"accuracy": 0.4, "params": [0.12, 0.90]},
127
+ ... "node_b": {"accuracy": 0.9, "params": [0.11, 0.79]},
128
+ ... },
129
+ ... ]
130
+ >>> results = Results(values)
131
+ >>> results.schema.rows
132
+ ["node_a", "node_b", "node_c"]
133
+ >>> results.schema.columns
134
+ ["accuracy", "params"]
135
+ >>> results.iterations
136
+ [0, 1]
137
+ >>> results(0)
138
+ [[0.7, [0.61, 0.72]], [0.1, [0.23, 0.37]], [0.8, [0.8, 0.84]]]
139
+ >>> results("node_a")
140
+ [[0.7, [0.61, 0.72]], [0.4, [0.12, 0.9]]]
141
+ >>> results("accuracy")
142
+ [[0.7, 0.1, 0.8], [0.4, 0.9, 0.1]]
143
+ >>> results([0], "accuracy")
144
+ [[0.7, 0.1, 0.8]]
145
+ """
146
+
147
+ __slots__ = ["schema", "values"]
148
+
149
+ def __init__(self, values: List[Dict[str, Dict[str, Any]]]):
150
+ self.schema = Schema(values)
151
+ self.values = list(map(self.schema.parse, values))
152
+
153
+ @property
154
+ def iterations(self) -> List[int]:
155
+ """
156
+ Return list of iterations
157
+
158
+ Returns
159
+ -------
160
+ List[int]
161
+ Iterations
162
+ """
163
+ return list(range(len(self.values)))
164
+
165
+ def sort_queries(
166
+ self,
167
+ indices: Tuple[List[int], List[int], List[int]],
168
+ query: Union[int, List[int], str],
169
+ ) -> Tuple[List[int], List[int], List[int]]:
170
+ """
171
+ Convert the query into index and add it to its corresponding
172
+ list of indices
173
+
174
+ Parameters
175
+ ----------
176
+ indices : Tuple[List[int], List[int], List[int]],
177
+ Iteration indices, row indices, column indices
178
+ query : Union[int, List[int], str]
179
+ Query
180
+
181
+ Returns
182
+ -------
183
+ Tuple[List[int], List[int], List[int]]
184
+ Updated iteration indices, row indices, column indices
185
+
186
+ Raises
187
+ ------
188
+ TypeError
189
+ When query input is a list which not contains only integers
190
+ KeyError
191
+ When query can not be found in keys defined by :code:`self.values`
192
+ """
193
+ it_indices, row_indices, column_indices = indices
194
+ if isinstance(query, int):
195
+ it_indices.append(query)
196
+ elif isinstance(query, list):
197
+ if not all(map(is_int, query)):
198
+ args = ", ".join(map(repr, query))
199
+ raise TypeError(
200
+ "Only integers in list queries are supported. "
201
+ f"Maybe try `results(..., {args})`."
202
+ )
203
+ it_indices.extend(query)
204
+ elif isinstance(query, str):
205
+ if query in self.schema.unordered_rows:
206
+ row_indices.append(self.schema.rows.index(query))
207
+ elif query in self.schema.unordered_columns:
208
+ column_indices.append(self.schema.columns.index(query))
209
+ else:
210
+ raise KeyError(f"Item {query!r} not found in schema.")
211
+ return (it_indices, row_indices, column_indices)
212
+
213
+ def __call__(self, *query: Union[str, int, List[int]]) -> list:
214
+ """
215
+ Return a list filtered by queries and ordered by :code:`self.schema`
216
+
217
+ Parameters
218
+ ----------
219
+ *query : Union[str, int, List[int]]
220
+ It can be an iteration, a list of iterations, a tag or a node ID
221
+
222
+ Returns
223
+ -------
224
+ list
225
+ Filtered list of results
226
+ """
227
+ if not query:
228
+ return self.values
229
+ it_indices, row_indices, column_indices = reduce(
230
+ self.sort_queries, query, ([], [], [])
231
+ )
232
+ values = itemgetter(*it_indices)(self.values) if it_indices else self.values
233
+ values = [values] if it_indices and (row_indices or column_indices) else values
234
+ values = map(itemgetter(*row_indices), values) if row_indices else values
235
+ values = [values] if row_indices and column_indices else values
236
+ values = (
237
+ (list(map(itemgetter(*column_indices), matrix)) for matrix in values)
238
+ if column_indices
239
+ else values
240
+ )
241
+ return list(values)
242
+
243
+ def __str__(self): # pragma: no cover
244
+ return (
245
+ f"Results(len(iterations)={len(self.iterations)},"
246
+ f" len(nodes)={len(self.schema.unordered_rows)}, "
247
+ f"len(tags)={len(self.schema.unordered_columns)})"
248
+ )
249
+
250
+ def __repr__(self): # pragma: no cover
251
+ return str(self)