remotivelabs-cli 0.0.1a35__tar.gz → 0.0.1a36__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/PKG-INFO +50 -1
  2. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/README.md +49 -0
  3. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/brokers.py +2 -2
  4. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/recordings.py +55 -38
  5. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/rest_helper.py +8 -3
  6. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/pyproject.toml +1 -1
  7. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/LICENSE +0 -0
  8. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/__about__.py +0 -0
  9. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/__init__.py +0 -0
  10. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/broker/brokers.py +0 -0
  11. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/broker/export.py +0 -0
  12. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/broker/files.py +0 -0
  13. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/broker/lib/__about__.py +0 -0
  14. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/broker/lib/broker.py +0 -0
  15. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/broker/lib/errors.py +0 -0
  16. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/broker/playback.py +0 -0
  17. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/broker/record.py +0 -0
  18. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/broker/scripting.py +0 -0
  19. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/broker/signals.py +0 -0
  20. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/__init__.py +0 -0
  21. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/auth.py +0 -0
  22. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/auth_tokens.py +0 -0
  23. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/cloud_cli.py +0 -0
  24. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/configs.py +0 -0
  25. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/projects.py +0 -0
  26. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/service_account_tokens.py +0 -0
  27. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/cloud/service_accounts.py +0 -0
  28. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/remotive.py +0 -0
  29. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/requirements.txt +0 -0
  30. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/settings.py +0 -0
  31. {remotivelabs_cli-0.0.1a35 → remotivelabs_cli-0.0.1a36}/cli/test/test_simple.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: remotivelabs-cli
3
- Version: 0.0.1a35
3
+ Version: 0.0.1a36
4
4
  Summary:
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
@@ -28,6 +28,55 @@ Read more at https://docs.remotivelabs.com/docs/cloud-cli/Installation
28
28
 
29
29
  You can use our CLI against both cloud and broker.
30
30
 
31
+ ## Cloud
32
+
33
+ Here summary of what you can do with the cli, for a complete list of cloud features
34
+ visit https://docs.remotivelabs.com/docs/category/remotivecloud/
35
+
36
+ The CLI does not currently support everything but we are working on having them in pair.
37
+
38
+ * remotive cloud auth
39
+ * Login / Logout
40
+ * Manage personal and service account tokens
41
+
42
+ * remotive cloud brokers
43
+ * Create broker
44
+ * Delete broker
45
+ * List brokers
46
+ * View broker logs
47
+
48
+ * remotive cloud licenses
49
+ * View physical broker licenses for your organisation
50
+
51
+ * remotive cloud recordings
52
+ * Upload recording
53
+ * Play recording
54
+ * List recordings
55
+ * Download recordings
56
+ * Upload custom configuration (i.e signal transformation)
57
+ * Download custom configurations
58
+ * Delete recordings and configurations
59
+
60
+ * remotive cloud signal-databases
61
+ * Upload
62
+ * Download
63
+ * Delete
64
+
65
+ * remotive cloud projects
66
+ * Create
67
+ * Delete
68
+ * List
69
+
70
+ * remotve cloud service-accounts
71
+ * Create
72
+ * Delete
73
+ * List
74
+
75
+
76
+ ### Broker
77
+
78
+ remotive cloud auth
79
+
31
80
  ## Subscribe to signals
32
81
 
33
82
  Output from running subscribe against our demo. You can visit https://demoo.remotivelabs.com
@@ -8,6 +8,55 @@ Read more at https://docs.remotivelabs.com/docs/cloud-cli/Installation
8
8
 
9
9
  You can use our CLI against both cloud and broker.
10
10
 
11
+ ## Cloud
12
+
13
+ Here summary of what you can do with the cli, for a complete list of cloud features
14
+ visit https://docs.remotivelabs.com/docs/category/remotivecloud/
15
+
16
+ The CLI does not currently support everything but we are working on having them in pair.
17
+
18
+ * remotive cloud auth
19
+ * Login / Logout
20
+ * Manage personal and service account tokens
21
+
22
+ * remotive cloud brokers
23
+ * Create broker
24
+ * Delete broker
25
+ * List brokers
26
+ * View broker logs
27
+
28
+ * remotive cloud licenses
29
+ * View physical broker licenses for your organisation
30
+
31
+ * remotive cloud recordings
32
+ * Upload recording
33
+ * Play recording
34
+ * List recordings
35
+ * Download recordings
36
+ * Upload custom configuration (i.e signal transformation)
37
+ * Download custom configurations
38
+ * Delete recordings and configurations
39
+
40
+ * remotive cloud signal-databases
41
+ * Upload
42
+ * Download
43
+ * Delete
44
+
45
+ * remotive cloud projects
46
+ * Create
47
+ * Delete
48
+ * List
49
+
50
+ * remotve cloud service-accounts
51
+ * Create
52
+ * Delete
53
+ * List
54
+
55
+
56
+ ### Broker
57
+
58
+ remotive cloud auth
59
+
11
60
  ## Subscribe to signals
12
61
 
13
62
  Output from running subscribe against our demo. You can visit https://demoo.remotivelabs.com
@@ -22,14 +22,14 @@ def describe(name: str, project: str = typer.Option(..., help="Project ID", envv
22
22
  rest.handle_get(f"/api/project/{project}/brokers/{name}")
23
23
 
24
24
 
25
- @app.command("remove", help="Stops and removes a broker from project")
25
+ @app.command("delete", help="Stops and deletes a broker from project")
26
26
  def stop(name: str, project: str = typer.Option(..., help="Project ID", envvar='REMOTIVE_CLOUD_PROJECT')):
27
27
  with Progress(
28
28
  SpinnerColumn(),
29
29
  TextColumn("[progress.description]{task.description}"),
30
30
  transient=True,
31
31
  ) as progress:
32
- progress.add_task(description=f"Stopping broker {name}...", total=None)
32
+ progress.add_task(description=f"Deleting broker {name}...", total=None)
33
33
  rest.handle_delete(f"/api/project/{project}/brokers/{name}")
34
34
 
35
35
 
@@ -18,8 +18,9 @@ def uid(p):
18
18
  print(p)
19
19
  return p['uid']
20
20
 
21
+
21
22
  # to be used in options
22
- #autocompletion=project_names)
23
+ # autocompletion=project_names)
23
24
  def project_names():
24
25
  r = requests.get(f'{rest.base_url}/api/bu/{rest.org}/project', headers=rest.headers)
25
26
  # sys.stderr.write(r.text)
@@ -40,7 +41,8 @@ def project_names():
40
41
 
41
42
 
42
43
  @app.command("list")
43
- def listRecordings(is_processing: bool = typer.Option(default=False, help="Use this option to see only those that are beeing processed or are invalid"),
44
+ def listRecordings(is_processing: bool = typer.Option(default=False,
45
+ help="Use this option to see only those that are beeing processed or are invalid"),
44
46
  project: str = typer.Option(..., help="Project ID", envvar='REMOTIVE_CLOUD_PROJECT')):
45
47
  """
46
48
  List all recording sessions in a project. You can choose to see all valid recordings (default) or use
@@ -54,7 +56,6 @@ def listRecordings(is_processing: bool = typer.Option(default=False, help="Use t
54
56
  rest.handle_get(f"/api/project/{project}/files/recording")
55
57
 
56
58
 
57
-
58
59
  @app.command(help="Shows details about a specific recording in project")
59
60
  def describe(recording_session: str = typer.Argument(..., help="Recording session id"),
60
61
 
@@ -72,10 +73,11 @@ def doStart(name: str, project: str, api_key: str, return_response: bool = False
72
73
 
73
74
 
74
75
  @app.command(help="Plays all recording files or a single recording")
75
- def play(recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
76
+ def play(recording_session: str = typer.Argument(..., help="Recording session id",
77
+ envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
76
78
  broker: str = typer.Option(..., help="Broker name to play on"),
77
79
  ensure_broker_started: bool = typer.Option(default=False, help="Ensure broker exists, start otherwise"),
78
- broker_config_name: str = typer.Option("default", help="Specify a custom broker configuration to use" ),
80
+ broker_config_name: str = typer.Option("default", help="Specify a custom broker configuration to use"),
79
81
  project: str = typer.Option(..., help="Project ID", envvar='REMOTIVE_CLOUD_PROJECT')):
80
82
  with Progress(
81
83
  SpinnerColumn(),
@@ -84,8 +86,10 @@ def play(recording_session: str = typer.Argument(..., help="Recording session id
84
86
  ) as progress:
85
87
  rest.ensure_auth_token()
86
88
  v = progress.add_task(description=f"Verifying broker {broker} exists...", total=100)
89
+ r = rest.handle_get(url=f'/api/project/{project}/brokers/{broker}',
90
+ return_response=True,
91
+ allow_status_codes=[404])
87
92
 
88
- r = requests.get(f'{rest.base_url}/api/project/{project}/brokers/{broker}', headers=rest.headers, )
89
93
  progress.update(v, advance=100.0)
90
94
  if r.status_code == 404:
91
95
  if ensure_broker_started:
@@ -108,7 +112,7 @@ def play(recording_session: str = typer.Argument(..., help="Recording session id
108
112
  # rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}/upload",
109
113
  # params={'brokerName': broker})
110
114
  # else:
111
- broker_config_query=""
115
+ broker_config_query = ""
112
116
  if (broker_config_name != "default"):
113
117
  broker_config_query = f"?brokerConfigName={broker_config_name}"
114
118
 
@@ -119,7 +123,8 @@ def play(recording_session: str = typer.Argument(..., help="Recording session id
119
123
  @app.command(help="Downloads the specified recording file to disk")
120
124
  def download_recording_file(
121
125
  recording_file_name: str = typer.Argument(..., help="Recording file to download"),
122
- recording_session: str = typer.Option(..., help="Recording session id that this file belongs to", envvar='REMOTIVE_CLOUD_RECORDING_SESSION'),
126
+ recording_session: str = typer.Option(..., help="Recording session id that this file belongs to",
127
+ envvar='REMOTIVE_CLOUD_RECORDING_SESSION'),
123
128
  project: str = typer.Option(..., help="Project ID", envvar='REMOTIVE_CLOUD_PROJECT')):
124
129
  with Progress(
125
130
  SpinnerColumn(),
@@ -162,16 +167,20 @@ def delete(recording_session: str = typer.Argument(..., help="Recording session
162
167
 
163
168
  @app.command(name="delete-recording-file")
164
169
  def delete_recording_file(recording_file_name: str = typer.Argument(..., help="Recording file to download"),
165
- recording_session: str = typer.Option(..., help="Recording session id that this file belongs to", envvar='REMOTIVE_CLOUD_RECORDING_SESSION'),
166
- project: str = typer.Option(..., help="Project ID", envvar='REMOTIVE_CLOUD_PROJECT')):
170
+ recording_session: str = typer.Option(...,
171
+ help="Recording session id that this file belongs to",
172
+ envvar='REMOTIVE_CLOUD_RECORDING_SESSION'),
173
+ project: str = typer.Option(..., help="Project ID", envvar='REMOTIVE_CLOUD_PROJECT')):
167
174
  """
168
175
  Deletes the specified recording file
169
176
 
170
177
  """
171
- rest.handle_delete(f'/api/project/{project}/files/recording/{recording_session}/recording-file/{recording_file_name}')
178
+ rest.handle_delete(
179
+ f'/api/project/{project}/files/recording/{recording_session}/recording-file/{recording_file_name}')
180
+
172
181
 
173
182
  @app.command()
174
- def upload(path: str = typer.Argument(..., help="Path to valid recording to upload"),
183
+ def upload(path: str = typer.Argument(..., help="Path to valid recording to upload"),
175
184
  project: str = typer.Option(..., help="Project ID", envvar='REMOTIVE_CLOUD_PROJECT')):
176
185
  with Progress(
177
186
  SpinnerColumn(),
@@ -193,10 +202,12 @@ def upload(path: str = typer.Argument(..., help="Path to valid recording to upl
193
202
  else:
194
203
  print(r.text)
195
204
 
205
+
196
206
  @app.command()
197
207
  def upload_broker_configuration(
198
- directory:str = typer.Argument(..., help="Configuration directory"),
199
- recording_session: str = typer.Option(..., help="Recording session id", envvar='REMOTIVE_CLOUD_RECORDING_SESSION'),
208
+ directory: str = typer.Argument(..., help="Configuration directory"),
209
+ recording_session: str = typer.Option(..., help="Recording session id",
210
+ envvar='REMOTIVE_CLOUD_RECORDING_SESSION'),
200
211
  project: str = typer.Option(..., help="Project ID", envvar='REMOTIVE_CLOUD_PROJECT'),
201
212
  overwrite: bool = typer.Option(False, help="Overwrite existing configuration if it exists")
202
213
  ):
@@ -214,7 +225,7 @@ def upload_broker_configuration(
214
225
  # List files in specified directory. Look for interfaces.json and use that directory where this is located
215
226
  # as configuration home directory
216
227
  #
217
- files = list(filter(lambda item : "interfaces.json" in item, glob.iglob(directory + '**/**', recursive=True)))
228
+ files = list(filter(lambda item: "interfaces.json" in item, glob.iglob(directory + '**/**', recursive=True)))
218
229
  if len(files) == 0:
219
230
  sys.stderr.write("No interfaces.json found in directory, this file is required")
220
231
  raise typer.Exit(1)
@@ -227,15 +238,18 @@ def upload_broker_configuration(
227
238
  # Get the current details about broker configurations to see if a config with this
228
239
  # name already exists
229
240
  #
230
- task=progress.add_task(description=f"Preparing upload of {broker_config_dir_name}", total=1)
231
- details_resp=rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
232
- details=details_resp.json()
241
+ task = progress.add_task(description=f"Preparing upload of {broker_config_dir_name}", total=1)
242
+ details_resp = rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}",
243
+ return_response=True)
244
+ details = details_resp.json()
233
245
  existing_configs = details['brokerConfigurations']
234
246
  if (len(existing_configs) > 0):
235
- data=list(filter(lambda x : x['name'] == broker_config_dir_name, existing_configs))
247
+ data = list(filter(lambda x: x['name'] == broker_config_dir_name, existing_configs))
236
248
  if len(data) > 0:
237
249
  if (overwrite):
238
- rest.handle_delete(f'/api/project/{project}/files/recording/{recording_session}/configuration/{broker_config_dir_name}', quiet=True)
250
+ rest.handle_delete(
251
+ f'/api/project/{project}/files/recording/{recording_session}/configuration/{broker_config_dir_name}',
252
+ quiet=True)
239
253
  else:
240
254
  sys.stderr.write("Broker configuration already exists, use --overwrite to replace\n")
241
255
  raise typer.Exit(1)
@@ -244,26 +258,29 @@ def upload_broker_configuration(
244
258
  # From the list of files, create a tuple of local_path to the actual file
245
259
  # and a remote path as it should be stored in cloud
246
260
  #
247
- file_infos = list(map(lambda item :
248
- { 'local_path' : item,
249
- 'remote_path' : f"/{broker_config_dir_name}{item.rsplit(broker_config_dir_name,1)[-1]}"} ,glob.iglob(directory + '**/*.*', recursive=True)))
261
+ file_infos = list(map(lambda item:
262
+ {'local_path': item,
263
+ 'remote_path': f"/{broker_config_dir_name}{item.rsplit(broker_config_dir_name, 1)[-1]}"},
264
+ glob.iglob(directory + '**/*.*', recursive=True)))
250
265
 
251
266
  #
252
267
  # convert this remote paths and ask cloud to prepare upload urls for those
253
268
  #
254
269
  json_request_upload_urls_req = {
255
- 'name' : 'not_used',
256
- 'paths' : list(map(lambda x: x['remote_path'], file_infos))
270
+ 'name': 'not_used',
271
+ 'paths': list(map(lambda x: x['remote_path'], file_infos))
257
272
  }
258
273
 
259
- response = rest.handle_put(url = f'/api/project/{project}/files/recording/{recording_session}/configuration', return_response=True, body=json.dumps(json_request_upload_urls_req))
274
+ response = rest.handle_put(url=f'/api/project/{project}/files/recording/{recording_session}/configuration',
275
+ return_response=True, body=json.dumps(json_request_upload_urls_req))
260
276
  if (response.status_code != 200):
261
277
  print(f'Failed to prepare configuration upload')
262
278
  print(f'{response.text} - {response.status_code}')
263
279
  raise typer.Exit(1)
264
280
  progress.update(task, advance=1)
265
281
 
266
- task=progress.add_task(description=f"Uploading {broker_config_dir_name} ({len(file_infos)} files)", total=len(file_infos))
282
+ task = progress.add_task(description=f"Uploading {broker_config_dir_name} ({len(file_infos)} files)",
283
+ total=len(file_infos))
267
284
 
268
285
  #
269
286
  # Upload urls is a remote_path : upload_url dict
@@ -273,12 +290,12 @@ def upload_broker_configuration(
273
290
 
274
291
  # For each file - upload
275
292
  for file in file_infos:
276
- key=file['remote_path']
277
- path=file['local_path']
278
- url=upload_urls[key]
279
- upload_task=progress.add_task(description=f"Uploading {key}", total=1)
280
- headers={
281
- 'Content-Type' : "application/x-www-form-urlencoded"
293
+ key = file['remote_path']
294
+ path = file['local_path']
295
+ url = upload_urls[key]
296
+ upload_task = progress.add_task(description=f"Uploading {key}", total=1)
297
+ headers = {
298
+ 'Content-Type': "application/x-www-form-urlencoded"
282
299
  }
283
300
  r = requests.put(url, open(path, 'rb'), headers=headers)
284
301
  progress.update(task, advance=1)
@@ -292,11 +309,11 @@ def upload_broker_configuration(
292
309
  print(f"Successfully uploaded broker configuration {broker_config_dir_name}")
293
310
 
294
311
 
295
-
296
312
  @app.command(help="Downloads the specified broker configuration directory as zip file")
297
313
  def download_configuration(
298
314
  broker_config_name: str = typer.Argument(..., help="Broker config name"),
299
- recording_session: str = typer.Option(..., help="Recording session id", envvar='REMOTIVE_CLOUD_RECORDING_SESSION'),
315
+ recording_session: str = typer.Option(..., help="Recording session id",
316
+ envvar='REMOTIVE_CLOUD_RECORDING_SESSION'),
300
317
  project: str = typer.Option(..., help="Project ID", envvar='REMOTIVE_CLOUD_PROJECT')
301
318
  ):
302
319
  rest.ensure_auth_token()
@@ -311,18 +328,18 @@ def download_configuration(
311
328
  open(filename, 'wb').write(r.content)
312
329
  print(f'Downloaded file {filename}')
313
330
 
331
+
314
332
  @app.command(help="Downloads the specified broker configuration directory as zip file")
315
333
  def delete_configuration(
316
334
  broker_config_name: str = typer.Argument(..., help="Broker config name"),
317
- recording_session: str = typer.Option(..., help="Recording session id", envvar='REMOTIVE_CLOUD_RECORDING_SESSION'),
335
+ recording_session: str = typer.Option(..., help="Recording session id",
336
+ envvar='REMOTIVE_CLOUD_RECORDING_SESSION'),
318
337
  project: str = typer.Option(..., help="Project ID", envvar='REMOTIVE_CLOUD_PROJECT')
319
338
  ):
320
-
321
339
  rest.handle_delete(
322
340
  url=f"/api/project/{project}/files/recording/{recording_session}/configuration/{broker_config_name}")
323
341
 
324
342
 
325
-
326
343
  def get_filename_from_cd(cd):
327
344
  """
328
345
  Get filename from content-disposition
@@ -4,6 +4,7 @@ import requests
4
4
  import os
5
5
  import json
6
6
  import logging
7
+ from typing import List
7
8
  from pathlib import Path
8
9
  from os.path import exists
9
10
  import typer
@@ -64,14 +65,14 @@ def ensure_auth_token():
64
65
  }
65
66
 
66
67
 
67
- def handle_get(url, params=None, return_response: bool = False):
68
+ def handle_get(url, params=None, return_response: bool = False, allow_status_codes=None):
68
69
  if params is None:
69
70
  params = {}
70
71
  ensure_auth_token()
71
72
  r = requests.get(f'{base_url}{url}', headers=headers, params=params)
72
73
 
73
74
  if return_response:
74
- check_api_result(r)
75
+ check_api_result(r, allow_status_codes)
75
76
  return r
76
77
  print_api_result(r)
77
78
 
@@ -85,8 +86,12 @@ def has_access(url, params={}):
85
86
  return True
86
87
 
87
88
 
88
- def check_api_result(response):
89
+ def check_api_result(response, allow_status_codes: List[int] = None):
90
+
89
91
  if response.status_code > 299:
92
+
93
+ if allow_status_codes is not None and response.status_code in allow_status_codes:
94
+ return
90
95
  err_console.print(f':boom: [bold red]Got status code[/bold red]: {response.status_code}')
91
96
  if response.status_code == 401:
92
97
  err_console.print("Your token has expired, please login again")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "remotivelabs-cli"
3
- version = "0.0.1.alpha35"
3
+ version = "0.0.1.alpha36"
4
4
  description = ""
5
5
  authors = ["Johan Rask <johan.rask@remotivelabs.com>"]
6
6
  readme = "README.md"