ciocore 5.1.1__py2.py3-none-any.whl → 10.0.0b3__py2.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 (126) hide show
  1. ciocore/VERSION +1 -1
  2. ciocore/__init__.py +23 -1
  3. ciocore/api_client.py +655 -160
  4. ciocore/auth/__init__.py +5 -3
  5. ciocore/cli.py +501 -0
  6. ciocore/common.py +15 -13
  7. ciocore/conductor_submit.py +77 -60
  8. ciocore/config.py +127 -13
  9. ciocore/data.py +162 -77
  10. ciocore/docsite/404.html +746 -0
  11. ciocore/docsite/apidoc/api_client/index.html +3605 -0
  12. ciocore/docsite/apidoc/apidoc/index.html +909 -0
  13. ciocore/docsite/apidoc/config/index.html +1652 -0
  14. ciocore/docsite/apidoc/data/index.html +1553 -0
  15. ciocore/docsite/apidoc/hardware_set/index.html +2460 -0
  16. ciocore/docsite/apidoc/package_environment/index.html +1507 -0
  17. ciocore/docsite/apidoc/package_tree/index.html +2386 -0
  18. ciocore/docsite/assets/_mkdocstrings.css +16 -0
  19. ciocore/docsite/assets/images/favicon.png +0 -0
  20. ciocore/docsite/assets/javascripts/bundle.471ce7a9.min.js +29 -0
  21. ciocore/docsite/assets/javascripts/bundle.471ce7a9.min.js.map +7 -0
  22. ciocore/docsite/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  23. ciocore/docsite/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  24. ciocore/docsite/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  25. ciocore/docsite/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  26. ciocore/docsite/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  27. ciocore/docsite/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  28. ciocore/docsite/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  29. ciocore/docsite/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  30. ciocore/docsite/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  31. ciocore/docsite/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  32. ciocore/docsite/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  33. ciocore/docsite/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  34. ciocore/docsite/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  35. ciocore/docsite/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  36. ciocore/docsite/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  37. ciocore/docsite/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  38. ciocore/docsite/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  39. ciocore/docsite/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  40. ciocore/docsite/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  41. ciocore/docsite/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  42. ciocore/docsite/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  43. ciocore/docsite/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  44. ciocore/docsite/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  45. ciocore/docsite/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  46. ciocore/docsite/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  47. ciocore/docsite/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  48. ciocore/docsite/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  49. ciocore/docsite/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  50. ciocore/docsite/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  51. ciocore/docsite/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  52. ciocore/docsite/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  53. ciocore/docsite/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  54. ciocore/docsite/assets/javascripts/lunr/tinyseg.js +206 -0
  55. ciocore/docsite/assets/javascripts/lunr/wordcut.js +6708 -0
  56. ciocore/docsite/assets/javascripts/workers/search.b8dbb3d2.min.js +42 -0
  57. ciocore/docsite/assets/javascripts/workers/search.b8dbb3d2.min.js.map +7 -0
  58. ciocore/docsite/assets/stylesheets/main.3cba04c6.min.css +1 -0
  59. ciocore/docsite/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  60. ciocore/docsite/assets/stylesheets/palette.06af60db.min.css +1 -0
  61. ciocore/docsite/assets/stylesheets/palette.06af60db.min.css.map +1 -0
  62. ciocore/docsite/cmdline/docs/index.html +871 -0
  63. ciocore/docsite/cmdline/downloader/index.html +934 -0
  64. ciocore/docsite/cmdline/packages/index.html +878 -0
  65. ciocore/docsite/cmdline/uploader/index.html +995 -0
  66. ciocore/docsite/how-to-guides/index.html +869 -0
  67. ciocore/docsite/index.html +895 -0
  68. ciocore/docsite/logo.png +0 -0
  69. ciocore/docsite/objects.inv +0 -0
  70. ciocore/docsite/search/search_index.json +1 -0
  71. ciocore/docsite/sitemap.xml +3 -0
  72. ciocore/docsite/sitemap.xml.gz +0 -0
  73. ciocore/docsite/stylesheets/extra.css +26 -0
  74. ciocore/docsite/stylesheets/tables.css +167 -0
  75. ciocore/downloader/base_downloader.py +644 -0
  76. ciocore/downloader/download_runner_base.py +47 -0
  77. ciocore/downloader/job_downloader.py +119 -0
  78. ciocore/{downloader.py → downloader/legacy_downloader.py} +12 -9
  79. ciocore/downloader/log.py +73 -0
  80. ciocore/downloader/logging_download_runner.py +87 -0
  81. ciocore/downloader/perpetual_downloader.py +63 -0
  82. ciocore/downloader/registry.py +97 -0
  83. ciocore/downloader/reporter.py +135 -0
  84. ciocore/exceptions.py +8 -2
  85. ciocore/file_utils.py +51 -50
  86. ciocore/hardware_set.py +449 -0
  87. ciocore/loggeria.py +89 -20
  88. ciocore/package_environment.py +110 -48
  89. ciocore/package_query.py +182 -0
  90. ciocore/package_tree.py +319 -258
  91. ciocore/retry.py +0 -0
  92. ciocore/uploader/_uploader.py +547 -364
  93. ciocore/uploader/thread_queue_job.py +176 -0
  94. ciocore/uploader/upload_stats/__init__.py +3 -4
  95. ciocore/uploader/upload_stats/stats_formats.py +10 -4
  96. ciocore/validator.py +34 -2
  97. ciocore/worker.py +174 -151
  98. ciocore-10.0.0b3.dist-info/METADATA +928 -0
  99. ciocore-10.0.0b3.dist-info/RECORD +128 -0
  100. {ciocore-5.1.1.dist-info → ciocore-10.0.0b3.dist-info}/WHEEL +1 -1
  101. ciocore-10.0.0b3.dist-info/entry_points.txt +2 -0
  102. tests/instance_type_fixtures.py +175 -0
  103. tests/package_fixtures.py +205 -0
  104. tests/test_api_client.py +297 -12
  105. tests/test_base_downloader.py +104 -0
  106. tests/test_cli.py +149 -0
  107. tests/test_common.py +1 -7
  108. tests/test_config.py +40 -18
  109. tests/test_data.py +162 -173
  110. tests/test_downloader.py +118 -0
  111. tests/test_hardware_set.py +139 -0
  112. tests/test_job_downloader.py +213 -0
  113. tests/test_package_query.py +38 -0
  114. tests/test_package_tree.py +91 -291
  115. tests/test_submit.py +44 -18
  116. tests/test_uploader.py +1 -4
  117. ciocore/__about__.py +0 -10
  118. ciocore/cli/conductor.py +0 -191
  119. ciocore/compat.py +0 -15
  120. ciocore-5.1.1.data/scripts/conductor +0 -19
  121. ciocore-5.1.1.data/scripts/conductor.bat +0 -13
  122. ciocore-5.1.1.dist-info/METADATA +0 -408
  123. ciocore-5.1.1.dist-info/RECORD +0 -47
  124. tests/mocks/api_client_mock.py +0 -51
  125. /ciocore/{cli → downloader}/__init__.py +0 -0
  126. {ciocore-5.1.1.dist-info → ciocore-10.0.0b3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,449 @@
1
+ """
2
+ This module contains the **HardwareSet** class.
3
+
4
+ It is designed to allow submission tools and other clients that consume instance types to be able to display them in categories. This is particularly useful for UIs that utilize combo-boxes, since they can be orgnaized into a nested structure and displayed in a tree-like fashion.
5
+
6
+
7
+ """
8
+ import copy
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ # {
15
+ # "cores": 32,
16
+ # "description": "32 core, 120GB Mem (8 V100 GPUs with 16GB Mem)",
17
+ # "gpu": {
18
+ # "gpu_architecture": "NVIDIA Volta",
19
+ # "gpu_count": 8,
20
+ # "gpu_cuda_cores": 5120,
21
+ # "gpu_memory": "16",
22
+ # "gpu_model": "V100",
23
+ # "total_gpu_cuda_cores": 40960,
24
+ # "total_gpu_memory": "128"
25
+ # },
26
+ # "memory": "120",
27
+ # "name": "n1-standard-32-v1-8",
28
+ # "operating_system": "linux"
29
+ # },
30
+
31
+
32
+ DESCRIPTION_TEMPLATE_OS = {
33
+ "cpu": "{operating_system} {cores} core {memory}GB Mem",
34
+ "gpu": "{operating_system} {cores} core {memory}GB Mem ({gpu_count} {gpu_model} GPUs {gpu_memory}GB Mem)",
35
+ }
36
+ DESCRIPTION_TEMPLATE = {
37
+ "cpu": "{cores} core {memory}GB Mem",
38
+ "gpu": "{cores} core {memory}GB Mem ({gpu_count} {gpu_model} GPUs {gpu_memory}GB Mem)",
39
+ }
40
+
41
+ def flatten_dict(d):
42
+ flat_dict = {}
43
+ for key, value in d.items():
44
+ if isinstance(value, dict):
45
+ nested_dict = flatten_dict(value)
46
+ for nested_key, nested_value in nested_dict.items():
47
+ flat_dict[nested_key] = nested_value
48
+ else:
49
+ flat_dict[key] = value
50
+ return flat_dict
51
+
52
+
53
+ class HardwareSet(object):
54
+ """A class to manage categorized instance types.
55
+
56
+ A HardwareSet encapsulates the instance types available to an account. It accepts a flat list of instance types and builds a nested structure where those instance types exist in categories.
57
+
58
+ It keeps a dictionary of instance types (`instance_types`) with the name field as key. This allows easy lookup by name.
59
+
60
+ In addition, it keeps the nested structure of categories (`categories`) that contain the instance types. Each category is a dictionary with keys: `label`, `content`, and `order`.
61
+
62
+ `content` is a list of instance types in the category. The order is used to sort the categories. The order of the instance types within a category is determined by the number of cores and memory.
63
+
64
+ If all instance_types have not been assigned any categories, then the structure is built with two default categories: CPU and GPU.
65
+ """
66
+
67
+ def __init__(self, instance_types):
68
+ """Initialize the HardwareSet with a list of instance types.
69
+ Typically, you would access the HardwareSet through the ciocore.data.data() function, which initializes it for you. However, you can also initialize it directly with a list of instance types straight from ciocore.api_client. The difference being that the latter contains all instance types, whereas the former contains only the instance types compatible with the products you have specified, as well as being cached.
70
+
71
+ Args:
72
+ instance_types (list): A list of instance types.
73
+
74
+ Returns:
75
+ HardwareSet: The initialized HardwareSet.
76
+
77
+ Examples:
78
+ ### Initialize with a list of instance types
79
+ >>> from ciocore import api_client
80
+ >>> from ciocore.hardware_set import HardwareSet
81
+ >>> instance_types = api_client.request_instance_types()
82
+ >>> hardware_set = HardwareSet(instance_types)
83
+ <ciocore.hardware_set.HardwareSet object at 0x104c43d30>
84
+
85
+ ### Initialize implicitly with a list of instance types from ciocore.data (recommended).
86
+ >>> from ciocore import data as coredata
87
+ >>> coredata.init("cinema4d")
88
+ >>> hardware_set = coredata.data()["instance_types"]
89
+ <ciocore.hardware_set.HardwareSet object at 0x104c43ee0>
90
+
91
+ !!! note
92
+ To avoid repetition, we use the implicit initialization for the examples below.
93
+ """
94
+
95
+ self.instance_types = self._build_unique(instance_types)
96
+ self.categories = self._build_categories()
97
+ self.provider = self._get_provider()
98
+
99
+ def labels(self):
100
+ """Get the list of category labels.
101
+
102
+ Returns:
103
+ list: A list of category labels.
104
+
105
+ Example:
106
+ >>> from ciocore import data as coredata
107
+ >>> coredata.init()
108
+ >>> hardware_set = coredata.data()["instance_types"]
109
+ >>> hardware_set.labels()
110
+ ['CPU', 'GPU']
111
+
112
+ """
113
+ return [c["label"] for c in self.categories]
114
+
115
+ def number_of_categories(self):
116
+ """Get the number of categories in the data.
117
+
118
+ Returns:
119
+ int: The number of categories.
120
+
121
+ Example:
122
+ >>> from ciocore import data as coredata
123
+ >>> coredata.init()
124
+ >>> hardware_set = coredata.data()["instance_types"]
125
+ >>> hardware_set.number_of_categories()
126
+ 2
127
+
128
+ """
129
+ return len(self.categories)
130
+
131
+ def recategorize(self, partitioner):
132
+ """Recategorize the instance types.
133
+
134
+ Args:
135
+ partitioner (function): A function that takes an instance type and returns a list of categories to assign to it. The function should return an empty list if the instance type should not be categorized.
136
+
137
+ Example:
138
+ # Confirm current categories
139
+ >>> from ciocore import data as coredata
140
+ >>> coredata.init()
141
+ >>> hardware_set = coredata.data()["instance_types"]
142
+ >>> print(hardware_set.labels()
143
+ ['CPU', 'GPU']
144
+
145
+ # Recategorize
146
+ >>> hardware_set.recategorize(lambda x: [{'label': 'Low cores', 'order': 10}] if x["cores"] < 16 else [{'label': 'High cores', 'order': 20}])
147
+ >>> print(hardware_set.labels()
148
+ ['Low cores', 'High cores']
149
+ """
150
+ for key in self.instance_types:
151
+ self.instance_types[key]["categories"] = partitioner(
152
+ self.instance_types[key]
153
+ )
154
+ self.categories = self._build_categories()
155
+
156
+ def find(self, name, category=None):
157
+ """Find an instance type by its name (sku).
158
+
159
+ Args:
160
+ name (str): The name of the instance type.
161
+
162
+ Returns:
163
+ dict: The instance type or None if not found.
164
+ Example:
165
+ >>> from ciocore import data as coredata
166
+ >>> coredata.init()
167
+ >>> hardware_set = coredata.data()["instance_types"]
168
+ >>> hardware_set.find("n2-highmem-80")
169
+ {
170
+ 'cores': 80,
171
+ 'description': '80 core, 640GB Mem',
172
+ 'gpu': None,
173
+ 'memory': '640',
174
+ 'name': 'n2-highmem-80',
175
+ 'operating_system': 'linux',
176
+ 'categories': [
177
+ {'label': 'High cores', 'order': 20}
178
+ ]
179
+ }
180
+
181
+ """
182
+ if not category:
183
+ return self.instance_types.get(name)
184
+
185
+ return self.find_first(
186
+ lambda x: x["name"] == name
187
+ and category in [c["label"] for c in x["categories"]]
188
+ )
189
+
190
+ def find_category(self, label):
191
+ """Find a category by label.
192
+
193
+ Args:
194
+ label (str): The label of the category.
195
+
196
+ Returns:
197
+ dict: The category or None if not found.
198
+ Example:
199
+ >>> from ciocore import data as coredata
200
+ >>> coredata.init()
201
+ >>> hardware_set = coredata.data()["instance_types"]
202
+ >>> hardware_set.find_category("High cores")
203
+ {
204
+ "label": "Low cores",
205
+ "content": [
206
+ {
207
+ "cores": 8,
208
+ "description": "8 core, 52GB Mem",
209
+ "gpu": None,
210
+ "memory": "52",
211
+ "name": "n1-highmem-8",
212
+ "operating_system": "linux",
213
+ "categories": [{"label": "Low cores", "order": 10}],
214
+ },
215
+ {
216
+ "cores": 8,
217
+ "description": "8 core, 7.2GB Mem",
218
+ "gpu": None,
219
+ "memory": "7.2",
220
+ "name": "n1-highcpu-8",
221
+ "operating_system": "linux",
222
+ "categories": [{"label": "Low cores", "order": 10}],
223
+ },
224
+ ...
225
+ ],
226
+ "order": 10
227
+ }
228
+ """
229
+ return next((c for c in self.categories if c["label"] == label), None)
230
+
231
+ def find_all(self, condition):
232
+ """Find all instance types that match a condition.
233
+
234
+ Args:
235
+ condition (function): A function that takes an instance type and returns True or False.
236
+
237
+ Returns:
238
+ list: A list of instance types that match the condition.
239
+
240
+ Example:
241
+ >>> from ciocore import data as coredata
242
+ >>> coredata.init()
243
+ >>> hardware_set = coredata.data()["instance_types"]
244
+ >>> hardware_set.find_all(lambda x: x["gpu"])
245
+ [
246
+ {
247
+ "cores": 4,
248
+ "description": "4 core, 15GB Mem (1 T4 Tensor GPU with 16GB Mem)",
249
+ "gpu": {
250
+ "gpu_architecture": "NVIDIA Turing",
251
+ "gpu_count": 1,
252
+ "gpu_cuda_cores": 2560,
253
+ "gpu_memory": "16",
254
+ "gpu_model": "T4 Tensor",
255
+ "gpu_rt_cores": 0,
256
+ "gpu_tensor_cores": 0,
257
+ "total_gpu_cuda_cores": 2560,
258
+ "total_gpu_memory": "16",
259
+ "total_gpu_rt_cores": 0,
260
+ "total_gpu_tensor_cores": 0,
261
+ },
262
+ "memory": "15",
263
+ "name": "n1-standard-4-t4-1",
264
+ "operating_system": "linux",
265
+ "categories": [{"label": "Low cores", "order": 10}],
266
+ },
267
+ {
268
+ "cores": 8,
269
+ "description": "8 core, 30GB Mem (1 T4 Tensor GPU with 16GB Mem)",
270
+ "gpu": {
271
+ "gpu_architecture": "NVIDIA Turing",
272
+ "gpu_count": 1,
273
+ "gpu_cuda_cores": 2560,
274
+ "gpu_memory": "16",
275
+ "gpu_model": "T4 Tensor",
276
+ "gpu_rt_cores": 0,
277
+ "gpu_tensor_cores": 0,
278
+ "total_gpu_cuda_cores": 2560,
279
+ "total_gpu_memory": "16",
280
+ "total_gpu_rt_cores": 0,
281
+ "total_gpu_tensor_cores": 0,
282
+ },
283
+ "memory": "30",
284
+ "name": "n1-standard-8-t4-1",
285
+ "operating_system": "linux",
286
+ "categories": [{"label": "Low cores", "order": 10}],
287
+ },
288
+ ...
289
+ ]
290
+ """
291
+ result = []
292
+ for key in self.instance_types:
293
+ if condition(self.instance_types[key]):
294
+ result.append(self.instance_types[key])
295
+ return result
296
+
297
+ def find_first(self, condition):
298
+ """Find the first instance type that matches a condition.
299
+
300
+ Please see find_all() above for more details. This method is just a convenience wrapper around find_all() that returns the first result or None if not found.
301
+
302
+ Args:
303
+ condition (function): A function that takes an instance type and returns True or False.
304
+
305
+ Returns:
306
+ dict: The first instance type that matches the condition or None if not found.
307
+ """
308
+ return next(iter(self.find_all(condition)), None)
309
+
310
+ # DEPRECATED
311
+ def get_model(self, with_misc=False):
312
+ """Get the categories structure with renaming ready for some specific widget, such as a Qt Combobox.
313
+
314
+ Deprecated:
315
+ The get_model() method is deprecated. The `with_misc` parameter is no longer used, which means that this function only serves to rename a few keys. What's more, the init function ensures that every instance type has a category. This function is no longer needed. Submitters that use it will work but should be updated to use the categories structure directly as it minimizes the levels of indirection necessary to work with it.
316
+ """
317
+ if with_misc:
318
+ logger.warning("with_misc is no longer used")
319
+ result = []
320
+ for category in self.categories:
321
+ result.append(
322
+ {
323
+ "label": category["label"],
324
+ "content": [
325
+ {"label": k["description"], "value": k["name"]}
326
+ for k in category["content"]
327
+ ],
328
+ }
329
+ )
330
+
331
+ return result
332
+
333
+ # PRIVATE METHODS
334
+ @classmethod
335
+ def _build_unique(cls, instance_types):
336
+ """Build a dictionary of instance types using the name field as key. This allows fast lookup by name.
337
+
338
+ Args:
339
+ instance_types (list): A list of instance types.
340
+
341
+ Returns:
342
+ dict: A dictionary of instance types with the name field as key.
343
+ """
344
+
345
+ instance_types = cls._rewrite_descriptions(instance_types)
346
+ categories = [
347
+ category
348
+ for it in instance_types
349
+ for category in (it.get("categories") or [])
350
+ ]
351
+ result = {}
352
+ for it in instance_types:
353
+ is_gpu = it.get("gpu", False)
354
+ if categories:
355
+ if it.get("categories") in [[], None]:
356
+ continue
357
+ else:
358
+ # make our own categories GPU/CPU
359
+ it["categories"] = (
360
+ [{"label": "GPU", "order": 2}]
361
+ if is_gpu
362
+ else [{"label": "CPU", "order": 1}]
363
+ )
364
+ result[it["name"]] = it
365
+
366
+ return result
367
+
368
+
369
+
370
+
371
+ @classmethod
372
+ def _rewrite_descriptions(cls, instance_types):
373
+ """Rewrite the descriptions of the instance types.
374
+
375
+ If there are both OS types, then the descriptions are prefixed with the OS type.
376
+
377
+ Args:
378
+ instance_types (list): A list of instance types.
379
+
380
+ Returns:
381
+ list: A list of instance types with rewritten descriptions.
382
+ """
383
+ if not instance_types:
384
+ return instance_types
385
+
386
+ first_os = instance_types[0]["operating_system"]
387
+ dual_platforms = next((it for it in instance_types if it["operating_system"] != first_os), False)
388
+
389
+ if dual_platforms:
390
+ for it in instance_types:
391
+ flat_dict = flatten_dict(it)
392
+ is_gpu = "gpu_count" in flat_dict
393
+ if is_gpu:
394
+ it["description"] = DESCRIPTION_TEMPLATE_OS["gpu"].format(**flat_dict)
395
+ else:
396
+ it["description"] = DESCRIPTION_TEMPLATE_OS["cpu"].format(**flat_dict)
397
+ else:
398
+ for it in instance_types:
399
+ flat_dict = flatten_dict(it)
400
+ is_gpu = "gpu_count" in flat_dict
401
+ if is_gpu:
402
+ it["description"] = DESCRIPTION_TEMPLATE["gpu"].format(**flat_dict)
403
+ else:
404
+ it["description"] = DESCRIPTION_TEMPLATE["cpu"].format(**flat_dict)
405
+
406
+ return instance_types
407
+
408
+ def _build_categories(self):
409
+ """Build a sorted list of categories where each category contains a sorted list of machines.
410
+
411
+ Returns:
412
+ list: A list of categories where each category is a dictionary with keys: `label`, `content`, and `order`.
413
+ """
414
+
415
+ dikt = {}
416
+ for key in self.instance_types:
417
+ it = self.instance_types[key]
418
+ categories = it["categories"]
419
+ for category in categories:
420
+ label = category["label"]
421
+ if label not in dikt:
422
+ dikt[label] = {
423
+ "label": label,
424
+ "content": [],
425
+ "order": category["order"],
426
+ }
427
+ dikt[label]["content"].append(it)
428
+
429
+ result = []
430
+ for label in dikt:
431
+ category = dikt[label]
432
+ category["content"].sort(key=lambda k: (k["cores"], k["memory"]))
433
+ result.append(category)
434
+ return sorted(result, key=lambda k: k["order"])
435
+
436
+ def _get_provider(self):
437
+ """Get the provider from the first instance type.
438
+
439
+ Returns:
440
+ str: The provider.
441
+ """
442
+ first_name = next(iter(self.instance_types))
443
+ if not first_name:
444
+ return None
445
+ if first_name.startswith("cw-"):
446
+ return "cw"
447
+ if "." in first_name:
448
+ return "aws"
449
+ return "gcp"
ciocore/loggeria.py CHANGED
@@ -3,10 +3,11 @@ import logging.handlers
3
3
  import multiprocessing
4
4
  import os
5
5
  import sys
6
+ import tempfile
6
7
  import threading
7
8
  import traceback
8
- from logging.handlers import TimedRotatingFileHandler
9
- from ciocore.common import CONDUCTOR_LOGGER_NAME
9
+
10
+ from . import config
10
11
 
11
12
  LEVEL_CRITICAL = "CRITICAL"
12
13
  LEVEL_ERROR = "ERROR"
@@ -29,12 +30,15 @@ LEVEL_MAP = {
29
30
 
30
31
 
31
32
  FORMATTER_LIGHT = logging.Formatter("%(asctime)s %(name)s: %(message)s", "%Y-%m-%d %H:%M:%S")
32
- FORMATTER_VERBOSE = logging.Formatter("%(asctime)s %(name)s%(levelname)9s: %(message)s")
33
+ FORMATTER_VERBOSE = logging.Formatter("%(asctime)s %(name)s%(levelname)9s %(filename)s-%(lineno)d %(threadName)s: %(message)s")
33
34
  DEFAULT_LEVEL_CONSOLE = LEVEL_MAP[LEVEL_INFO]
34
35
  DEFAULT_LEVEL_FILE = LEVEL_MAP[LEVEL_DEBUG]
35
36
  DEFAULT_LEVEL_LOGGER = LEVEL_MAP[LEVEL_INFO]
36
37
 
37
38
  CONDUCTOR_LOGGER_NAME = "conductor"
39
+ __logger__ = None
40
+
41
+ LOG_PATH = None
38
42
 
39
43
 
40
44
  class LogLevelFilter(logging.Filter):
@@ -56,16 +60,19 @@ class LogLevelFilter(logging.Filter):
56
60
  return int(record.levelno < logging.ERROR)
57
61
 
58
62
 
59
- def setup_conductor_logging(
63
+ def setup_conductor_logging(
60
64
  logger_level=DEFAULT_LEVEL_LOGGER,
61
65
  console_level=None,
62
66
  console_formatter=FORMATTER_LIGHT,
63
- log_filepath=None,
67
+ log_dirpath=None,
64
68
  file_level=None,
65
69
  file_formatter=FORMATTER_VERBOSE,
66
70
  multiproc=False,
67
71
  disable_console_logging = False,
68
- propagate = True
72
+ propagate = True,
73
+ log_filename=None,
74
+ use_system_log=False
75
+
69
76
  ):
70
77
  """The is convenience function to help set up logging.
71
78
 
@@ -87,6 +94,11 @@ def setup_conductor_logging(
87
94
  multiproc: bool. If True, a custom file handler will be used that handles multiprocess logging
88
95
  correctly. This file handler creates an additional Process.
89
96
  """
97
+ global __logger__
98
+
99
+ if __logger__:
100
+ return
101
+
90
102
  # Get the top/parent conductor logger
91
103
  logger = get_conductor_logger()
92
104
 
@@ -118,38 +130,71 @@ def setup_conductor_logging(
118
130
  console_handler_out.setFormatter(console_formatter)
119
131
  console_handler_err.setFormatter(console_formatter)
120
132
 
121
- # Create a file handler if a filepath was given
122
- if log_filepath:
133
+ # Create a file handler. Use the given path or fallback to the default path.
134
+ if log_filename:
123
135
  if file_level:
124
136
  assert file_level in LEVEL_MAP.values(), "Not a valid log level: %s" % file_level
137
+
138
+ if use_system_log and not log_dirpath:
139
+ log_dirpath = get_system_log_dir()
140
+
141
+ log_dirpath = log_dirpath or get_user_log_dir()
142
+
125
143
  # Rotating file handler. Rotates every day (24 hours). Stores 7 days at a time.
126
144
  file_handler = create_file_handler(
127
- log_filepath, level=file_level, formatter=file_formatter, multiproc=multiproc
145
+ filepath=log_dirpath,
146
+ filename=log_filename,
147
+ level=file_level,
148
+ formatter=file_formatter,
149
+ multiproc=multiproc,
150
+ use_fallback_path=True
128
151
  )
129
152
  logger.addHandler(file_handler)
130
153
 
154
+ __logger__ = logger
131
155
 
132
- def create_file_handler(filepath, level=None, formatter=None, multiproc=False):
133
- """Create a file handler object for the given filepath.
156
+
157
+ def create_file_handler(filepath, filename, level=None, formatter=None, multiproc=False, use_fallback_path=True):
158
+ """Create a file handler object in the given filepath with filename.
134
159
 
135
160
  This is a ROTATING file handler, which rotates every day (24 hours) and stores up to 7 days of
136
161
  logs at a time (equaling up to as many as 7 log files at a given time.
162
+
163
+ If writing to the given filepath fails due to permissions and use_fallback path is True, the
164
+ logger will try to use the users home folder.
137
165
  """
166
+
138
167
  when = "h" # rotate unit is "h" (hours)
139
168
  interval = 24 # rotate every 24 units (24 hours)
140
169
  backupCount = 7 # Retain up to 7 log files (7 days of log files)
170
+
171
+ try:
172
+ if not os.path.exists(filepath):
173
+ os.makedirs(filepath)
174
+
175
+ else:
176
+ tempfile.TemporaryFile(dir=filepath)
177
+
178
+ except PermissionError as error_msg:
141
179
 
142
- log_dirpath = os.path.dirname(filepath)
143
- if not os.path.exists(log_dirpath):
144
- os.makedirs(log_dirpath)
180
+ print("Unable to use {} for logs. ({})".format(filepath, error_msg))
181
+
182
+ if filepath != get_user_log_dir and use_fallback_path:
183
+ return create_file_handler(filepath=get_user_log_dir(),
184
+ filename=filename,
185
+ formatter=formatter,
186
+ multiproc=multiproc)
187
+
188
+ else:
189
+ raise
145
190
 
146
191
  if multiproc:
147
192
  # Use custom rotating file handler that handles multiprocessing properly
148
- handler = MPFileHandler(filepath, when=when, interval=interval, backupCount=backupCount)
193
+ handler = MPFileHandler(os.path.join(filepath, filename), when=when, interval=interval, backupCount=backupCount)
149
194
  else:
150
195
  # Rotating file handler. Rotates every day (24 hours). Stores 7 days at a time.
151
196
  handler = logging.handlers.TimedRotatingFileHandler(
152
- filepath, when=when, interval=interval, backupCount=backupCount
197
+ os.path.join(filepath, filename), when=when, interval=interval, backupCount=backupCount
153
198
  )
154
199
  if formatter:
155
200
  handler.setFormatter(formatter)
@@ -157,6 +202,9 @@ def create_file_handler(filepath, level=None, formatter=None, multiproc=False):
157
202
  if level:
158
203
  handler.setLevel(level)
159
204
 
205
+ global LOG_PATH
206
+ LOG_PATH=os.path.join(filepath, filename)
207
+
160
208
  return handler
161
209
 
162
210
 
@@ -184,7 +232,7 @@ class MPFileHandler(logging.Handler):
184
232
  ):
185
233
  """See TimedRotatingFileHandler for arg docs."""
186
234
  logging.Handler.__init__(self)
187
- self._handler = TimedRotatingFileHandler(
235
+ self._handler = logging.handlers.TimedRotatingFileHandler(
188
236
  filename,
189
237
  when=when,
190
238
  interval=interval,
@@ -359,9 +407,30 @@ class TableStr(object):
359
407
  def get_footer(self):
360
408
  return self.footer
361
409
 
362
- def get_default_log_dir(platform=None):
410
+ def get_default_log_dir(platform=None, system_log=False):
411
+
412
+ if system_log:
413
+ log_dir = get_system_log_dir(platform)
414
+
415
+ if os.access(log_dir, os.W_OK|os.X_OK):
416
+ return log_dir
417
+
418
+ return get_user_log_dir(platform=platform)
419
+
420
+ def get_system_log_dir(platform=None):
421
+
422
+ platform = platform or sys.platform
423
+
424
+ if platform.startswith('win'):
425
+ return os.path.expandvars(os.path.join("%programdata%", "conductor", "logs"))
363
426
 
364
- #platform = platform or sys.platform
427
+ else:
428
+ return "/var/log/conductor"
429
+
430
+ def get_user_log_dir(platform=None):
365
431
 
366
- return os.path.expanduser(os.path.join("~", ".conductor", "logs", "conductor.log"))
432
+ root_path = config.get()['user_dir']
433
+ return os.path.expanduser(os.path.join(root_path, "logs"))
434
+
435
+
367
436