ciocore 8.0.0b32__py2.py3-none-any.whl → 8.0.0rc1__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.

Potentially problematic release.


This version of ciocore might be problematic. Click here for more details.

ciocore/VERSION CHANGED
@@ -1 +1 @@
1
- 8.0.0-beta.32
1
+ 8.0.0-rc.1
ciocore/cli.py CHANGED
@@ -131,7 +131,15 @@ def _register(client):
131
131
  @click.pass_context
132
132
  @click.option("-v", "--version", is_flag=True, help="Print the version and exit.")
133
133
  def main(ctx, version):
134
- """Conductor Command-line interface."""
134
+ """
135
+ Conductor Command-line interface.
136
+
137
+ To get help on subcommands, use the --help flag after the subcommand.
138
+
139
+ Example:
140
+ conductor download --help
141
+
142
+ """
135
143
  if not ctx.invoked_subcommand:
136
144
  if version:
137
145
  click.echo(VERSION)
@@ -374,11 +382,9 @@ def downloader(
374
382
  )
375
383
  ctx.exit(0)
376
384
 
377
- if task_id:
378
- job_id = job_id + ":" + task_id
379
-
380
385
  Downloader.download_jobs(
381
386
  (job_id,),
387
+ task_id=task_id,
382
388
  thread_count=thread_count,
383
389
  output_dir=output,
384
390
  )
Binary file
@@ -1,8 +1,8 @@
1
1
 
2
2
 
3
3
  :root {
4
- --md-primary-fg-color: #a71ed8;
5
- --md-accent-fg-color: #db4ae3;
4
+ --md-primary-fg-color: #3c75a4;
5
+ --md-accent-fg-color: #406499;
6
6
  }
7
7
 
8
8
  .md-content a {
@@ -158,9 +158,6 @@
158
158
  .opts-prop-create {
159
159
  color: #36be00
160
160
  }
161
- /* .opts-prop-query {
162
- color: #af00db
163
- } */
164
161
  .opts-prop-edit {
165
162
  color: #007dd1
166
163
  }
@@ -18,10 +18,10 @@ import time
18
18
  import threading
19
19
  import hashlib
20
20
 
21
+ import ciocore
21
22
  from ciocore import config
22
23
  from ciocore import api_client, common, loggeria
23
24
  from ciocore.common import CONDUCTOR_LOGGER_NAME
24
- from cioseq.sequence import Sequence
25
25
 
26
26
  try:
27
27
  import Queue as queue
@@ -291,12 +291,12 @@ class Downloader(object):
291
291
  downloader.print_uptime()
292
292
 
293
293
  @classmethod
294
- def download_jobs(cls, job_ids, thread_count=None, output_dir=None):
294
+ def download_jobs(cls, job_ids, task_id=None, thread_count=None, output_dir=None):
295
295
  """
296
296
  Run the downloader for explicit jobs, and terminate afterwards.
297
297
  """
298
298
  downloader = cls(thread_count=thread_count, output_dir=output_dir)
299
- downloader.start(job_ids)
299
+ thread_states = downloader.start(job_ids, task_id=task_id)
300
300
  while not common.SIGINT_EXIT and (
301
301
  not downloader.pending_queue.empty() or not downloader.downloading_queue.empty()
302
302
  ):
@@ -306,7 +306,7 @@ class Downloader(object):
306
306
  downloader._print_download_history()
307
307
  downloader.print_uptime()
308
308
 
309
- def start(self, job_ids=None, summary_interval=10):
309
+ def start(self, job_ids=None, task_id=None, summary_interval=10):
310
310
  # Create new queues
311
311
  self.start_time = time.time()
312
312
  self.pending_queue = queue.Queue()
@@ -316,7 +316,7 @@ class Downloader(object):
316
316
  # If a job id has been specified then only load the queue up with that work
317
317
  if job_ids:
318
318
  self.history_queue_max = None
319
- self.get_jobs_downloads(job_ids)
319
+ self.get_jobs_downloads(job_ids, task_id)
320
320
 
321
321
  # otherwise create a queue thread the polls the app for wor
322
322
  else:
@@ -461,12 +461,10 @@ class Downloader(object):
461
461
  # Wait for the queue to be consumed before querying for more
462
462
  self.nap()
463
463
 
464
-
465
464
  def nap(self):
466
465
  while not common.SIGINT_EXIT:
467
466
 
468
467
  time.sleep(self.naptime)
469
- # Pretty sure this return should be unindented!
470
468
  return
471
469
 
472
470
  @common.dec_timer_exit(log_level=logging.DEBUG)
@@ -479,51 +477,23 @@ class Downloader(object):
479
477
  except Exception as e:
480
478
  logger.exception("Could not get next download")
481
479
 
482
- def get_jobs_downloads(self, job_ids):
480
+ def get_jobs_downloads(self, job_ids, task_id):
483
481
  """
484
- Download jobs, each with an optional task specification.
485
-
486
- Job ids are padded to 5 digits, and task ids are padded to 3 digits.
482
+ Get each job and optional comma-separated task list.
487
483
 
488
- job_ids: (tuple) may take any of the following forms:
489
- 1. A single job id, e.g. (01234,)
490
- 2. It doesn't have to be padded, e.g. (1234,)
491
- 3. A jobs with a task specification, e.g. (01234:1-3,5,7-9)
492
- 4. Several of jobs:tasks, e.g. (01234, 56789:1-3,5,7-9)
493
-
494
- """
495
- jobs = self._flatten(job_ids)
496
- for job in jobs:
497
- endpoint = self.endpoint_downloads_job % job["job_id"]
498
-
499
- for tid in job["tasks"] or [None]:
500
- downloads = _get_job_download(endpoint, self.api_client, job["job_id"], tid)
484
+ There will only be a task list if there is just one job, due to earlier arg validation.
485
+
486
+ If there is no task list, _get_job_download is called with tid=None (i.e. the whole job)
487
+ """
488
+ task_ids = [t for t in task_id.split(",") if t] if task_id else [None]
489
+
490
+ for job_id in job_ids:
491
+ endpoint = self.endpoint_downloads_job % job_id
492
+ for tid in task_ids:
493
+ downloads = _get_job_download(endpoint, self.api_client, job_id, tid)
501
494
  if downloads:
502
495
  for task_download in downloads.get("downloads", []):
503
- print("putting in queue: %s" % task_download)
504
496
  self.pending_queue.put(task_download, block=True)
505
-
506
- @staticmethod
507
- def _flatten(job_ids):
508
- """Create a list of job objects with keys: job_id and tasks.
509
-
510
- see: get_jobs_downloads() function see tests/test_downloader.py for examples.
511
- """
512
- result = []
513
- for job_id in job_ids:
514
- if ":" in job_id:
515
- job_id, range_spec = job_id.split(":")
516
- try:
517
- seq = Sequence.create(range_spec)
518
- tasks = seq.expand("###")
519
- except (ValueError, TypeError):
520
- tasks = None
521
- else:
522
- tasks = None
523
- result.append({"job_id": job_id.zfill(5), "tasks": tasks})
524
- return result
525
-
526
-
527
497
 
528
498
  @common.dec_catch_exception(raise_=True)
529
499
  def download_target(self, pending_queue, downloading_queue, task_download_state):
@@ -620,8 +590,8 @@ class Downloader(object):
620
590
  # account for a render's generated subdirectories
621
591
  dirpath = os.path.dirname(local_filepath)
622
592
 
623
- # Ensure that the output_path directory exists and set open permissions
624
- logger.debug("Creating output_path directory if necessary: %s", dirpath)
593
+ # Ensure that the destination directory exists and set open permissions
594
+ logger.debug("Creating destination directory if necessary: %s", dirpath)
625
595
  file_download_state.status = FileDownloadState.STATE_PREPARING_DIRECTORY
626
596
  safe_mkdirs(dirpath)
627
597
 
@@ -1268,7 +1238,7 @@ def run_downloader(args):
1268
1238
  file_formatter=LOG_FORMATTER,
1269
1239
  )
1270
1240
 
1271
- api_client.ApiClient.register_client(client_name = Downloader.CLIENT_NAME, client_version=__version__)
1241
+ api_client.ApiClient.register_client(client_name = Downloader.CLIENT_NAME, client_version=ciocore.__version__)
1272
1242
 
1273
1243
  logger.debug("Downloader args: %s", args)
1274
1244
 
@@ -1278,6 +1248,7 @@ def run_downloader(args):
1278
1248
  if job_ids:
1279
1249
  Downloader.download_jobs(
1280
1250
  job_ids,
1251
+ task_id=args.get("task_id"),
1281
1252
  thread_count=thread_count,
1282
1253
  output_dir=args.get("output"),
1283
1254
  )
@@ -1306,6 +1277,6 @@ def report_error(self, download_id, error_message):
1306
1277
  resp_str, resp_code = self.api_helper.make_request(
1307
1278
  "/downloads/%s/fail" % download_id, data=error_message, verb="POST", use_api_key=True
1308
1279
  )
1309
- except:
1280
+ except e:
1310
1281
  pass
1311
1282
  return True
ciocore/file_utils.py CHANGED
@@ -256,13 +256,13 @@ def get_common_dirpath(paths):
256
256
 
257
257
 
258
258
  def _is_valid_path(path_str):
259
- """
259
+ r"""
260
260
  This is dirty/inaccurate helper function to determine whether the given "path" is considered
261
261
  valid. If so, return True.
262
262
 
263
263
  If the given path_str is any of the following characters, then it's to be considered invalid:
264
264
 
265
- On linux\mac:
265
+ On linux/mac:
266
266
  /
267
267
  //
268
268
  lettered drive (e.g. x:\)
@@ -453,7 +453,7 @@ def get_tx_path(filepath, existing_only=False):
453
453
 
454
454
 
455
455
  def strip_drive_letter(filepath):
456
- """
456
+ r"""
457
457
  If the given filepath has a drive letter, remove it and return the rest of the path
458
458
 
459
459
  C:\cat.txt --> \cat.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ciocore
3
- Version: 8.0.0b32
3
+ Version: 8.0.0rc1
4
4
  Summary: Core functionality for Conductor's client tools
5
5
  Home-page: https://github.com/ConductorTechnologies/ciocore
6
6
  Author: conductor
@@ -51,8 +51,7 @@ See [CONTRIBUTING](CONTRIBUTING.md)
51
51
 
52
52
  ## Changelog
53
53
 
54
- ## Version: 8.0.0-beta.30 -- 30 Mar 2024
55
-
54
+ ## Version:8.0.0-rc.1 -- 05 Apr 2024
56
55
 
57
56
  Major Updates in Version 8.0.0
58
57
 
@@ -1,16 +1,15 @@
1
- ciocore/VERSION,sha256=CNZsvBEqWiYC9vIgbPM4bF6nCIO2d2TSSVgiI35UuBQ,13
1
+ ciocore/VERSION,sha256=oaAkSyjgoKqpHRAw-3LYleaedh24W2mf8BvHs5j0-ak,10
2
2
  ciocore/__init__.py,sha256=aTP7LeeosQA8BZE67gDV4jgfTK5zxmwZRjiTRu_ZWj0,646
3
3
  ciocore/api_client.py,sha256=TyHXGmK4uTmKV93O_IpAvttZhbexIIZTGP-IVn_WyWM,24599
4
- ciocore/cli.py,sha256=BXgULpqElTHwX6nUcSFVVh99MCBSG2FjuOzRKhIa6Hk,15399
4
+ ciocore/cli.py,sha256=9OdoazUovfUHxpiAHahH7HJjJcdRLO0_QsdPXjGjlW8,15509
5
5
  ciocore/client_db.py,sha256=tTz3bl2xeDPPcYSDS3g3QgV_xYihJMx0Kj6OeN2klK0,12978
6
6
  ciocore/common.py,sha256=mBIS6KiYoQsjWe6aIFUGRRvCMl8BIN2kmLZ4J8icap8,14982
7
7
  ciocore/compat.py,sha256=5uEXPSog_jxsDMaHBswAKEtfyXT25VgU6WNGIhz9PHU,256
8
8
  ciocore/conductor_submit.py,sha256=ONE0LsA5hGavTJIOXXYx8qzl8_vBPADwhd6Ytq_0E0c,9382
9
9
  ciocore/config.py,sha256=rCL7kaFn1tYgSglN8q9Wx6SwMpoXTq0BMQGwPRVwVIg,8973
10
10
  ciocore/data.py,sha256=Ji0qUk8nJXBNakoHSqBiVx8O58SbZXyt273SHlEDn3U,7027
11
- ciocore/dev_inst_tagger.py,sha256=bPfoFbE7IPCpDbtbVHxW7IlMwvspdnJhPuNq-u4fJN4,3497
12
11
  ciocore/exceptions.py,sha256=4Oq-WX-qiN6kPUdBCHvvd6mtSQ0nCkDbJxWt2CNtpv8,1504
13
- ciocore/file_utils.py,sha256=bAlL31B4YkRgX-yT8kF8UXBFktQlsE1PvxbKqTeAeOU,17174
12
+ ciocore/file_utils.py,sha256=swA7th9WhDEloW69YViRTKB-oeC9UmNdEGegfH1r-Gw,17176
14
13
  ciocore/hardware_set.py,sha256=FlRQiGCLRcSW7Oko_gzgVK8ZqJ_J92eT8e_AleAbS2E,17047
15
14
  ciocore/loggeria.py,sha256=2xdQRFb9NyXynU2O_pSOszJWcpoHgPwTUWJvERg7ODY,15251
16
15
  ciocore/package_environment.py,sha256=MEHV7jfs3NJIEYCIaW8JfJdBmelvPHZMmBzPlXETiRo,7808
@@ -27,7 +26,7 @@ ciocore/docsite/index.html,sha256=p_Dq_6_8cwZZPDI5zjahfCWUkfbSj14NJtoQFAZv0WI,20
27
26
  ciocore/docsite/logo.png,sha256=gArgFFWdw8w985-0TkuGIgU_pW9sziEMZdqytXb5WLo,2825
28
27
  ciocore/docsite/objects.inv,sha256=s2FKStLlVIQbfG7U2-nw-7rz4unvd1W0u00YtLBxAKo,758
29
28
  ciocore/docsite/sitemap.xml,sha256=M_V85zl0y2adRvzJAnoCxlZH_Hl7TLnIb1A-6l_xGmI,109
30
- ciocore/docsite/sitemap.xml.gz,sha256=TKsxI6-9nU-iyOIEJTL1yHZV8KoBweu_CZYLcbZMynM,127
29
+ ciocore/docsite/sitemap.xml.gz,sha256=5eSPEBgK40Dy0VTOM8gl-guV4pLORNHnn-M_KPwVDQk,127
31
30
  ciocore/docsite/apidoc/api_client/index.html,sha256=mHDq-zdWuilDKUluzmQNdOsImRoGEnnK5Ul2ApajgdU,170218
32
31
  ciocore/docsite/apidoc/apidoc/index.html,sha256=NQn8wjapxa7O2IQhsbE7Y-VMfMFL6Tby-L7qIF6K3m4,26171
33
32
  ciocore/docsite/apidoc/config/index.html,sha256=1GWkyClM7LJlLBpx07BqCi3xDkU-SKVPjNUEeKuwbNs,72559
@@ -83,13 +82,13 @@ ciocore/docsite/cmdline/packages/index.html,sha256=j1YeMgdwZ6FyJGJii05AdtUWKRjyu
83
82
  ciocore/docsite/cmdline/uploader/index.html,sha256=y0728lIAYC4b9mfpjNVfwQqXoqJrsdG49UY5u7B1p94,25123
84
83
  ciocore/docsite/how-to-guides/index.html,sha256=OFrFBTb9tuMn60Jd4oeM10Mw-d-0pyqwoKoo9g1biBM,20100
85
84
  ciocore/docsite/search/search_index.json,sha256=ZMAbHLAlwOTujgA32XJTA7SHogRYh9i-ioPFOxjjJ0s,182633
86
- ciocore/docsite/stylesheets/extra.css,sha256=qgfcao9TEBzf6ieAWN5rPyrea7YM_YgVg5qr4NPrxoU,354
87
- ciocore/docsite/stylesheets/tables.css,sha256=O2PEwlKC0RfvZCRV9UERqQRvhb6jcqO84PLew0bafF0,3279
85
+ ciocore/docsite/stylesheets/extra.css,sha256=_Cxe9Dhg1BBi6Kqaz_iZD9z9VyqxA9vtONRjP4PVic0,354
86
+ ciocore/docsite/stylesheets/tables.css,sha256=LE_zwGRxGcdPIy-9QiVPecOzlEBSqZb_WP5vDkFE0ZM,3235
88
87
  ciocore/downloader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
88
  ciocore/downloader/base_downloader.py,sha256=9PVmP1qy-dop4MbVsmLFvf5W6LKQLUKHPDYE6AEt71g,25784
90
89
  ciocore/downloader/download_runner_base.py,sha256=cWTWKhFX5FLG84Wh8_s7xpk3n0nmQ4fDiD2rYV6a3u0,1612
91
90
  ciocore/downloader/job_downloader.py,sha256=HAhr95RyCsj3KTp5W58Znc4JmqHY-mrM5b8vo6kRIJ0,5697
92
- ciocore/downloader/legacy_downloader.py,sha256=ef9hCMUTTuxYHKzpEoK5bc3gqwAEIhewkC_KpbhLVHE,52087
91
+ ciocore/downloader/legacy_downloader.py,sha256=LIbT3BxuY56lq_UW_io9FSgK_2VIKwFVMENyF-5KllA,51158
93
92
  ciocore/downloader/log.py,sha256=WCRNx0LObx8JBO5MQucNHQGBXMgSBLGdH0VALN8dFDo,2105
94
93
  ciocore/downloader/logging_download_runner.py,sha256=iOuW8OvfAYW5MpP9zpUXh94qkWLC0GeG4PDGH7JSqJM,2865
95
94
  ciocore/downloader/perpetual_downloader.py,sha256=cD7lnBH75-c-ZVVPHZc1vSnDhgJOnGlPT85zn7IjGgA,2105
@@ -121,8 +120,8 @@ tests/test_uploader.py,sha256=B1llTJt_fqR6e_V_Jxfw9z73QgkFlEPU87xLYGzt-TQ,2914
121
120
  tests/test_validator.py,sha256=2fY66ayNc08PGyj2vTI-V_1yeCWJDngkj2zkUM5TTCI,1526
122
121
  tests/mocks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
123
122
  tests/mocks/glob.py,sha256=J2MH7nqi6NJOHuGdVWxhfeBd700_Ckj6cLh_8jSNkfg,215
124
- ciocore-8.0.0b32.dist-info/METADATA,sha256=-fliSdqGREA2a5rnAhFe-yqaLDS5NJuKI_IUzlvHei4,18201
125
- ciocore-8.0.0b32.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
126
- ciocore-8.0.0b32.dist-info/entry_points.txt,sha256=cCqcALMYbC4d8545V9w0Zysfg9MVuKWhzDQ2er4UfGE,47
127
- ciocore-8.0.0b32.dist-info/top_level.txt,sha256=SvlM5JlqULzAz00JZWfiUhfjhqDzYzSWssA87zdJl0o,14
128
- ciocore-8.0.0b32.dist-info/RECORD,,
123
+ ciocore-8.0.0rc1.dist-info/METADATA,sha256=OiC555SBSXja3tKUjJCh3NWrvYpRdzUTLGHP4nSlMaY,18196
124
+ ciocore-8.0.0rc1.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
125
+ ciocore-8.0.0rc1.dist-info/entry_points.txt,sha256=cCqcALMYbC4d8545V9w0Zysfg9MVuKWhzDQ2er4UfGE,47
126
+ ciocore-8.0.0rc1.dist-info/top_level.txt,sha256=SvlM5JlqULzAz00JZWfiUhfjhqDzYzSWssA87zdJl0o,14
127
+ ciocore-8.0.0rc1.dist-info/RECORD,,
@@ -1,120 +0,0 @@
1
- """
2
- DEV only. This module is used to request instance types from the inst-tagger service. We try to isolate the cod so it can be easily removed.
3
- """
4
-
5
- import os
6
- import requests
7
- from ciocore import api_client
8
-
9
- USE_INST_TAGGER = int(os.environ.get("CIO_FEATURE_USE_INST_TAGGER", 0))
10
- SUPABASE_URL = os.environ.get("CIO_FEATURE_INST_TAGGER_SUPABASE_URL")
11
- SUPABASE_KEY = os.environ.get("CIO_FEATURE_INST_TAGGER_SUPABASE_KEY")
12
-
13
- def request_supabase_data(table_name):
14
- HEADERS = {
15
- "Authorization": f"Bearer {SUPABASE_KEY}",
16
- "apikey": SUPABASE_KEY,
17
- "Content-Type": "application/json",
18
- }
19
-
20
- response = requests.get(
21
- f"{SUPABASE_URL}/rest/v1/{table_name}", headers=HEADERS, timeout=5
22
- )
23
- if response.status_code == 200:
24
- return response.json()
25
- else:
26
- print("Error:", response.status_code, response.text)
27
- return None
28
-
29
-
30
- def get_cloud(cio_inst_types):
31
- if not cio_inst_types:
32
- return None
33
- if cio_inst_types[0]["name"].startswith("cw-"):
34
- return "cw"
35
- elif "." in cio_inst_types[0]["name"]:
36
- return "aws"
37
- return "gcp"
38
-
39
-
40
- def normalize_sb_inst_types(sb_inst_types):
41
- result = []
42
- for inst_type in sb_inst_types:
43
- el = inst_type["instance_type"].copy()
44
- el["id"] = inst_type["id"]
45
- result.append(el)
46
- return result
47
-
48
-
49
- def to_id_dict(thelist):
50
- return {d["id"]: d for d in thelist}
51
-
52
-
53
- def valid_inst_types():
54
- sb_inst_types = request_supabase_data("instance_types")
55
- sb_inst_types = normalize_sb_inst_types(sb_inst_types)
56
-
57
- cio_inst_types = api_client.request_instance_types()
58
- # print(json.dumps(cio_inst_types, indent=2))
59
-
60
- cloud = get_cloud(cio_inst_types)
61
-
62
- # filter out instance types that are not in the cloud
63
-
64
- sb_inst_types = [
65
- inst_type for inst_type in sb_inst_types if inst_type["cloud"] == cloud
66
- ]
67
- for it in sb_inst_types:
68
- it["cores"] = it["total_cpu"]
69
- it["memory"] = it["total_memory_gb"]
70
-
71
- cores = it["cores"]
72
- mem = it["memory_gb"]
73
- cpu_desc = f"{cores} cores, {mem} GB RAM"
74
- gpu_desc = ""
75
- if it["gpu"]:
76
- gpu_count = it["gpu"]["gpu_count"]
77
- gpu_model = it["gpu"]["gpu_model"]
78
- gpu_arch = it["gpu"]["gpu_architecture"]
79
- gpu_mem = it["gpu"]["total_gpu_memory"]
80
- gpu_desc = f" ({gpu_count}x {gpu_model} ({gpu_arch}) {gpu_mem} GB)"
81
-
82
- desc = f"{cpu_desc}{gpu_desc}"
83
- it["description"] = desc
84
-
85
- return sb_inst_types
86
-
87
-
88
- def request_instance_types():
89
- """
90
- Request instance types from the inst-tagger service.
91
-
92
- Returns:
93
- list(dict): A list of instance types.
94
- """
95
- if not USE_INST_TAGGER:
96
- return None
97
-
98
- inst_types = valid_inst_types()
99
-
100
- tags = request_supabase_data("tags")
101
- tag_ids = [tag["id"] for tag in tags]
102
- tags_dict = to_id_dict(tags)
103
-
104
- assignments = request_supabase_data("assignments")
105
- assignments = [a for a in assignments if a["tag_id"] in tag_ids]
106
-
107
-
108
- # assign tags to instance types
109
- for inst_type in inst_types:
110
- inst_type["categories"] = []
111
- # find assignments relating to this instance type
112
- these_assignments = [
113
- a for a in assignments if a["instance_type_id"] == inst_type["id"]
114
- ]
115
-
116
- for assignment in these_assignments:
117
- tag = tags_dict[assignment["tag_id"]]
118
- inst_type["categories"].append({"label": tag["label"], "order": tag["order"]})
119
-
120
- return inst_types