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
@@ -6,18 +6,18 @@ import getpass
6
6
  import json
7
7
  import logging
8
8
  import os
9
- import re
10
9
  import threading
10
+ import traceback
11
11
 
12
12
  from ciocore import config
13
- from ciocore import exceptions, file_utils, api_client, uploader
14
-
13
+ from ciocore import file_utils, api_client, uploader, exceptions
15
14
  from ciocore.common import CONDUCTOR_LOGGER_NAME
16
-
15
+
17
16
  logger = logging.getLogger(CONDUCTOR_LOGGER_NAME)
18
17
 
19
18
  FEATURE_DEV = int(os.environ.get("CIO_FEATURE_DEV", 0))
20
19
 
20
+
21
21
  class Submit(object):
22
22
  """Conductor Submission object."""
23
23
 
@@ -33,7 +33,7 @@ class Submit(object):
33
33
  self.enforced_md5s = args.get("enforced_md5s", {})
34
34
  self.database_filepath = args.get("database_filepath", "")
35
35
  self.api_client = api_client.ApiClient()
36
-
36
+
37
37
  self.progress_handler = None
38
38
  self.uploader_ = None
39
39
 
@@ -70,16 +70,25 @@ class Submit(object):
70
70
  self.payload[arg] = args[arg]
71
71
  except KeyError:
72
72
  if default is None:
73
- logger.error("Submit: You must provide the '{}' argument.".format(arg))
73
+ logger.error(
74
+ "Submit: You must provide the '{}' argument.".format(arg)
75
+ )
74
76
  raise
75
77
 
78
+ # If no upload paths are provided, make sure the backend does not expect a daemon to be running.
79
+ if not self.upload_paths:
80
+ self.payload["local_upload"] = True
81
+
76
82
  # HACK: Posix -> Windows submission - must windowize output_path. Only available for
77
83
  # developers. If a customer tries to submit from Mac to Windows, then they have access to
78
84
  # Windows instances by mistake. Yes this code could get them out of a bind, but it will
79
85
  # generate support tickets when they try to use the uploader daemon for example.
80
- self.ensure_windows_drive_letters = FEATURE_DEV and self.payload["instance_type"].endswith("-w")
81
- self.payload["output_path"] = self._ensure_windows_drive_letter(self.payload["output_path"])
82
-
86
+ self.ensure_windows_drive_letters = FEATURE_DEV and self.payload[
87
+ "instance_type"
88
+ ].endswith("-w")
89
+ self.payload["output_path"] = self._ensure_windows_drive_letter(
90
+ self.payload["output_path"]
91
+ )
83
92
 
84
93
  self.payload["notify"] = {"emails": self.payload["notify"]}
85
94
 
@@ -88,25 +97,25 @@ class Submit(object):
88
97
  logger.debug("{}:{}".format(arg, self.payload[arg]))
89
98
 
90
99
  def upload_progress_callback(self, upload_stats):
91
- '''
100
+ """
92
101
  Call the progress handler
93
- '''
94
-
102
+ """
103
+
95
104
  if self.progress_handler:
96
- logger.debug("Sending progress update to {}".format(self.progress_handler))
105
+ logger.debug("Sending progress update to {}".format(self.progress_handler))
97
106
  self.progress_handler(upload_stats)
98
107
 
99
108
  def stop_work(self):
100
- '''
109
+ """
101
110
  Cancel the submission process
102
- '''
103
-
111
+ """
112
+
104
113
  logger.debug("Submitter was requested to stop work.")
105
114
 
106
115
  if self.uploader_:
107
- logger.debug("Uploader set to cancel.")
108
- self.uploader_.cancel=True
109
-
116
+ logger.debug("Uploader set to cancel.")
117
+ self.uploader_.cancel = True
118
+
110
119
  def main(self):
111
120
  """
112
121
  Submit the job
@@ -118,39 +127,42 @@ class Submit(object):
118
127
  2. local_upload=False: md5 calcs and uploads are performed on on any machine with access to
119
128
  the filesystem on which the files reside, and by the same paths as the submission machine.
120
129
  """
121
-
122
- self._log_threads(message_template="{thread_count} threads before starting upload")
123
130
 
124
- processed_filepaths = file_utils.process_upload_filepaths(self.upload_paths)
125
- file_map = {path: None for path in processed_filepaths}
131
+ self._log_threads(
132
+ message_template="{thread_count} threads before starting upload"
133
+ )
134
+
135
+ if self.upload_paths:
126
136
 
127
- if self.payload["local_upload"]:
128
- file_map = self._handle_local_upload(file_map)
129
-
130
- elif self.enforced_md5s:
131
- file_map = self._enforce_md5s(file_map)
137
+ processed_filepaths = file_utils.process_upload_filepaths(self.upload_paths)
138
+ file_map = {path: None for path in processed_filepaths}
132
139
 
133
- for path in file_map:
134
- expanded = self._expand_stats(path, file_map[path])
140
+ if self.payload["local_upload"]:
141
+ file_map = self._handle_local_upload(file_map)
135
142
 
136
-
137
- self.payload["upload_files"].append(expanded)
138
- self.payload["upload_size"] += expanded["st_size"]
143
+ elif self.enforced_md5s:
144
+ file_map = self._enforce_md5s(file_map)
139
145
 
146
+ for path in file_map:
147
+ expanded = self._expand_stats(path, file_map[path])
148
+ self.payload["upload_files"].append(expanded)
149
+ self.payload["upload_size"] += expanded["st_size"]
140
150
 
141
151
  self._log_threads(message_template="{thread_count} threads after upload")
142
-
152
+
143
153
  logger.info("Sending Job...")
144
-
154
+
145
155
  response, response_code = self.api_client.make_request(
146
- uri_path="jobs/",
147
- data=json.dumps(self.payload),
148
- raise_on_error=False,
149
- use_api_key=True
156
+ uri_path="jobs/",
157
+ data=json.dumps(self.payload),
158
+ raise_on_error=False,
159
+ use_api_key=True,
150
160
  )
151
-
161
+
152
162
  if response_code not in [201, 204]:
153
- raise Exception("Job Submission failed: Error %s ...\n%s" % (response_code, response))
163
+ raise Exception(
164
+ "Job Submission failed: Error %s ...\n%s" % (response_code, response)
165
+ )
154
166
 
155
167
  return json.loads(response), response_code
156
168
 
@@ -160,34 +172,42 @@ class Submit(object):
160
172
 
161
173
  Returns {"path1': md5_1, path2: md5_2}
162
174
  """
163
-
164
175
  cfg = config.config().config
165
176
  api_client.read_conductor_credentials(use_api_key=True)
166
-
177
+
167
178
  # Don't use more threads than there are files
168
179
  thread_count = min(len(file_map), cfg["thread_count"])
169
180
  logger.info("Using {} threads for the uploader".format(thread_count))
170
-
181
+
171
182
  uploader_args = {
172
183
  "location": self.payload["location"],
173
184
  "database_filepath": self.database_filepath,
174
185
  "thread_count": thread_count,
175
186
  "md5_caching": self.md5_caching,
176
187
  }
177
-
188
+
178
189
  self.uploader_ = uploader.Uploader(uploader_args)
179
190
 
180
191
  self.uploader_.progress_callback = self.upload_progress_callback
181
-
192
+
182
193
  self.uploader_.handle_upload_response(self.payload["project"], file_map)
183
-
194
+
184
195
  if self.uploader_.cancel:
185
- raise exceptions.UserCanceledError("Job submission was cancelled by the user")
196
+ raise exceptions.UserCanceledError(
197
+ "Job submission was cancelled by the user"
198
+ )
186
199
 
187
200
  if self.uploader_.error_messages:
188
- # Need a better way to handle multiple exceptions
189
- raise self.uploader_.error_messages[0][1]
190
-
201
+ error_message = ""
202
+ for cnt, err in enumerate(self.uploader_.error_messages):
203
+ error_message += "Error {}:\n{}\n\n".format(
204
+ cnt + 1, "".join(traceback.format_exception(*err))
205
+ )
206
+ raise Exception(
207
+ "\n\nCould not upload files, encountered %s errors:\n\n%s"
208
+ % (len(self.uploader_.error_messages), error_message)
209
+ )
210
+
191
211
  # Get the resulting dictionary of the file's and their corresponding md5 hashes
192
212
  upload_md5s = self.uploader_.return_md5s()
193
213
  for path in upload_md5s:
@@ -202,13 +222,13 @@ class Submit(object):
202
222
 
203
223
  Returns {"path1': enforced_md5_1, path2: enforced_md5_2}
204
224
  """
205
-
225
+
206
226
  progress_title = "Processing MD5 of local files"
207
227
  file_count = len(self.enforced_md5s)
208
228
 
209
229
  for cnt, filepath in enumerate(self.enforced_md5s):
210
- percentage_complete = float(cnt)/float(file_count)
211
-
230
+ percentage_complete = float(cnt) / float(file_count)
231
+
212
232
  md5 = self.enforced_md5s[filepath]
213
233
  logger.debug("filepath is %s" % filepath)
214
234
  processed_filepaths = file_utils.process_upload_filepath(filepath)
@@ -219,7 +239,6 @@ class Submit(object):
219
239
 
220
240
  return file_map
221
241
 
222
-
223
242
  def _expand_stats(self, file, md5):
224
243
  filestat = os.stat(file)
225
244
 
@@ -240,16 +259,15 @@ class Submit(object):
240
259
  "st_mtime": filestat.st_mtime,
241
260
  "st_ctime": filestat.st_ctime,
242
261
  }
243
-
262
+
244
263
  def _log_threads(self, message_template):
245
264
 
246
265
  threads = list(threading.enumerate())
247
-
266
+
248
267
  for t in threads:
249
268
  logger.debug(t)
250
269
 
251
- logger.debug(message_template.format(thread_count=len(threads)))
252
-
270
+ logger.debug(message_template.format(thread_count=len(threads)))
253
271
 
254
272
  def _ensure_windows_drive_letter(self, filepath):
255
273
  """
@@ -261,4 +279,3 @@ class Submit(object):
261
279
  logger.debug("Windows dev hack! Setting {0} to 'X:{0}'".format(filepath))
262
280
  return "X:{}".format(filepath)
263
281
  return filepath
264
-
ciocore/config.py CHANGED
@@ -1,49 +1,135 @@
1
1
  """
2
- Config is an Object containing configuration variables.
2
+ Config is a configuration object implemented as a module-level singleton.
3
3
 
4
- Only contains stuff that is not suitable to specify in a submitter.
4
+ Configuration variables can be shared by importing the module. If there are changes in environment variables or other sources, the config can be refreshed.
5
5
  """
6
6
 
7
7
  import logging
8
8
  import os
9
- import multiprocessing
9
+ # import multiprocessing
10
10
  import base64
11
11
  import json
12
12
  import re
13
- from ciocore import loggeria
13
+ import platform
14
14
 
15
15
  from ciocore.common import CONDUCTOR_LOGGER_NAME
16
16
 
17
17
  logger = logging.getLogger(CONDUCTOR_LOGGER_NAME)
18
18
 
19
- #https://stackoverflow.com/a/3809435/179412
20
- URL_REGEX=re.compile(r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)")
19
+ # https://stackoverflow.com/a/3809435/179412
20
+
21
+
22
+ USER_DIRS = {
23
+ "Linux": os.path.expanduser(os.path.join("~", ".conductor")),
24
+ "Darwin": os.path.expanduser(os.path.join("~",".conductor")),
25
+ "Windows": os.path.expanduser(os.path.join("~", "AppData", "Local", "Conductor")),
26
+ }
27
+
28
+ DEFAULT_USER_DIR = USER_DIRS.get(platform.system(), USER_DIRS["Linux"])
29
+
30
+ URL_REGEX = re.compile(
31
+ r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)"
32
+ )
21
33
 
22
34
  __config__ = None
23
35
 
36
+
24
37
  def config(force=False):
38
+ """
39
+ Instantiate a config object if necessary.
40
+
41
+ Deprecated:
42
+ Use [get()](#get) instead.
43
+
44
+ Args:
45
+ force (bool): Discards any existing config object and instantiate a new one -- Defaults to `False`.
46
+
47
+ Returns:
48
+ dict: A dictionary containing configuration values.
49
+ """
50
+
25
51
  global __config__
26
52
  if force or not __config__:
27
53
  __config__ = Config()
28
54
  return __config__
29
55
 
56
+
57
+ def get(force=False):
58
+ """
59
+ Instantiate a config object if necessary and return the dictionary.
60
+
61
+ Args:
62
+ force (bool): Discards any existing config object and instantiate a new one -- Defaults to `False`.
63
+
64
+ Returns:
65
+ dict: A dictionary containing configuration values.
66
+
67
+ Example:
68
+ >>> from ciocore import config
69
+ >>> config.get()
70
+ {
71
+ 'thread_count': 16,
72
+ 'priority': 5,
73
+ 'md5_caching': True,
74
+ 'log_level': 'INFO',
75
+ 'url': 'https://dashboard.conductortech.com',
76
+ 'auth_url': 'https://dashboard.conductortech.com',
77
+ 'api_url': 'https://api.conductortech.com',
78
+ 'api_key': None,
79
+ 'downloader_page_size': 50,
80
+ }
81
+ """
82
+ global __config__
83
+ if force or not __config__:
84
+ __config__ = Config()
85
+ return __config__.config
86
+
87
+
30
88
  class Config(object):
31
89
  def __init__(self):
90
+ """
91
+ Initialize the config object.
92
+
93
+ A config object is a dictionary containing configuration values. It is a singleton, so there is only one instance of it. It is instantiated the first time it is needed. It can be refreshed by calling get() with the `force` keyword argument set to `True`.
94
+
95
+ A Config object has the following properties:
96
+
97
+ * `thread_count` The number of threads to use for downloading files. Defaults to the number of CPUs on the system times 2. It can be overridden by the `CONDUCTOR_THREAD_COUNT` environment variable.
98
+ * `priority` Set the priority for submissions. Defaults to 5. It can be overridden by the `CONDUCTOR_PRIORITY` environment variable.
99
+ * `md5_caching` Whether to cache MD5s. Defaults to `True`. It can be overridden by the `CONDUCTOR_MD5_CACHING` environment variable. Cachine MD5s significantly improves submission performance, but on rare occasions it can cause submissions to fail. If you experience this, set `md5_caching` to `False`.
100
+ * `log_level` The logging level. Defaults to `INFO`. It can be overridden by the `CONDUCTOR_LOG_LEVEL` environment variable.
101
+ * `url` The URL of the Conductor dashboard. Defaults to `https://dashboard.conductortech.com`. It can be overridden by the `CONDUCTOR_URL` environment variable.
102
+ * `auth_url` The URL of the Conductor dashboard. Defaults to `https://dashboard.conductortech.com`. It can be overridden by the `CONDUCTOR_AUTH_URL` environment variable. This is deprecated. Use `url` instead.
103
+ * `api_url` The URL of the Conductor API. Defaults to `https://api.conductortech.com`. It can be overridden by the `CONDUCTOR_API_URL` environment variable.
104
+ * `api_key` The API key. The API key can be acquired from the Conductor dashboard, and can be stored in an environment variable or a file. In both cases the API KEY can be a JSON object or a base64 encoded JSON object. If it is base64 encoded, it can be a string or bytes. If it is a string, it will be decoded as ASCII. If it is bytes, it will be decoded as UTF-8.
105
+ * Environment variable: The `CONDUCTOR_API_KEY` variable can hold the API KEY directly.
106
+ * File: The `CONDUCTOR_API_KEY_PATH` variable can hold the path to a file containing the API KEY.
107
+ * `downloader_page_size` The number of files to request from the Conductor API at a time. Defaults to 50. It can be overridden by the `CONDUCTOR_DOWNLOADER_PAGE_SIZE` environment variable.
108
+
109
+ Returns:
110
+ Config: A config object.
111
+
112
+ Raises:
113
+ ValueError -- Invalid inputs, such as badly formed URLs.
114
+ """
115
+ default_downloader_page_size = 50
116
+
117
+
32
118
 
33
119
  try:
34
- default_thread_count = min(multiprocessing.cpu_count() * 2, 16)
120
+ default_thread_count = min( os.cpu_count() - 1, 15)
35
121
  except NotImplementedError:
36
- default_thread_count = 16
122
+ default_thread_count = 15
37
123
 
38
124
  url = os.environ.get("CONDUCTOR_URL", "https://dashboard.conductortech.com")
39
125
 
40
126
  if not URL_REGEX.match(url):
41
127
  raise ValueError("CONDUCTOR_URL is not valid '{}'".format(url))
42
-
128
+
43
129
  api_url = os.environ.get("CONDUCTOR_API_URL", url.replace("dashboard", "api"))
44
130
  if not URL_REGEX.match(api_url):
45
131
  raise ValueError("CONDUCTOR_API_URL is not valid '{}'".format(api_url))
46
-
132
+
47
133
  falsy = ["false", "no", "off", "0"]
48
134
 
49
135
  log_level = os.environ.get("CONDUCTOR_LOG_LEVEL", "INFO")
@@ -51,23 +137,41 @@ class Config(object):
51
137
  log_level = "INFO"
52
138
 
53
139
  self.config = {
54
- "thread_count": int(os.environ.get("CONDUCTOR_THREAD_COUNT", default_thread_count)),
140
+ "thread_count": int(
141
+ os.environ.get("CONDUCTOR_THREAD_COUNT", default_thread_count)
142
+ ),
143
+ "downloader_page_size": int(
144
+ os.environ.get(
145
+ "CONDUCTOR_DOWNLOADER_PAGE_SIZE", default_downloader_page_size
146
+ )
147
+ ),
55
148
  "priority": int(os.environ.get("CONDUCTOR_PRIORITY", 5)),
56
149
  "md5_caching": False
57
150
  if os.environ.get("CONDUCTOR_MD5_CACHING", "True").lower() in falsy
58
151
  else True,
59
152
  "log_level": log_level,
60
153
  "url": url,
61
- # Keep "auth_url" for backwwards compatibillity only.
62
- # Clients should use "url" moving forward.
154
+ # Keep "auth_url" for backwwards compatibillity only.
155
+ # Clients should use "url" moving forward.
63
156
  # Remove "auth_url" on the next major version bump.
64
157
  "auth_url": url,
65
158
  "api_url": api_url,
66
159
  "api_key": self.get_api_key_from_variable() or self.get_api_key_from_file(),
160
+ "user_dir": os.environ.get('CONDUCTOR_USER_DIR', DEFAULT_USER_DIR)
67
161
  }
68
162
 
69
163
  @staticmethod
70
164
  def get_api_key_from_variable():
165
+ """
166
+ Attempt to get an API key from the `CONDUCTOR_API_KEY` environment variable.
167
+
168
+ Raises:
169
+ ValueError: An error occurred while reading or loading the key into JSON.
170
+
171
+ Returns:
172
+ str: JSON object containing the key - base 64 decoded if necessary.
173
+
174
+ """
71
175
  api_key = os.environ.get("CONDUCTOR_API_KEY")
72
176
  if not api_key:
73
177
  return
@@ -88,6 +192,16 @@ class Config(object):
88
192
 
89
193
  @staticmethod
90
194
  def get_api_key_from_file():
195
+ """
196
+ Attempt to get an API key from the file in the CONDUCTOR_API_KEY_PATH environment variable.
197
+
198
+ Raises:
199
+ ValueError: An error occurred while reading or loading the key into JSON.
200
+
201
+ Returns:
202
+ str: JSON object containing the key - base 64 decoded if necessary.
203
+
204
+ """
91
205
  api_key_path = os.environ.get("CONDUCTOR_API_KEY_PATH")
92
206
  if not api_key_path:
93
207
  return