besapi 3.7.8__tar.gz → 3.8.2__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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: besapi
3
- Version: 3.7.8
3
+ Version: 3.8.2
4
4
  Summary: Library for working with the BigFix REST API
5
5
  Home-page: https://github.com/jgstew/besapi
6
6
  Author: Matt Hansen, James Stewart
@@ -8,14 +8,27 @@ Author-email: hansen.m@psu.edu, james@jgstew.com
8
8
  License: MIT
9
9
  Keywords: bigfix iem tem rest api
10
10
  Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
11
12
  Classifier: Operating System :: OS Independent
12
13
  Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Systems Administration
13
18
  Requires-Python: >=3.7
14
19
  Description-Content-Type: text/markdown
15
20
  License-File: LICENSE.txt
16
21
  Requires-Dist: requests
17
22
  Requires-Dist: lxml
18
23
  Requires-Dist: cmd2
24
+ Requires-Dist: setuptools
25
+ Dynamic: author
26
+ Dynamic: author-email
27
+ Dynamic: home-page
28
+ Dynamic: keywords
29
+ Dynamic: license
30
+ Dynamic: requires-dist
31
+ Dynamic: summary
19
32
 
20
33
  # besapi
21
34
 
@@ -4,8 +4,13 @@ long_description = file: README.md
4
4
  long_description_content_type = text/markdown
5
5
  classifiers =
6
6
  Programming Language :: Python :: 3
7
+ Programming Language :: Python :: 3.9
7
8
  Operating System :: OS Independent
8
9
  License :: OSI Approved :: MIT License
10
+ Development Status :: 5 - Production/Stable
11
+ Intended Audience :: Developers
12
+ Topic :: Software Development :: Libraries :: Python Modules
13
+ Topic :: System :: Systems Administration
9
14
 
10
15
  [options]
11
16
  python_requires = >=3.7
@@ -1,14 +1,9 @@
1
1
  #!/usr/bin/env python
2
- """
3
- file to configure python build and packaging for pip
4
- """
2
+ """File to configure python build and packaging for pip."""
5
3
 
6
- try:
7
- from setuptools import setup
8
- except (ImportError, ModuleNotFoundError):
9
- from distutils.core import setup
4
+ import setuptools
10
5
 
11
- setup(
6
+ setuptools.setup(
12
7
  name="besapi",
13
8
  # version= moved to setup.cfg
14
9
  author="Matt Hansen, James Stewart",
@@ -20,7 +15,7 @@ setup(
20
15
  # long_description= moved to setup.cfg
21
16
  packages=["besapi", "bescli"],
22
17
  package_data={"besapi": ["schemas/*.xsd"]},
23
- install_requires=["requests", "lxml", "cmd2"],
18
+ install_requires=["requests", "lxml", "cmd2", "setuptools"],
24
19
  include_package_data=True,
25
20
  package_dir={"": "src"},
26
21
  )
@@ -1,6 +1,4 @@
1
- """
2
- besapi is a python module for interacting with the BigFix REST API
3
- """
1
+ """Besapi is a python module for interacting with the BigFix REST API."""
4
2
 
5
3
  # https://stackoverflow.com/questions/279237/import-a-module-from-a-relative-path/4397291
6
4
 
@@ -1,6 +1,4 @@
1
- """
2
- To run this module directly
3
- """
1
+ """To run this module directly."""
4
2
 
5
3
  import logging
6
4
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python
2
2
  """
3
- besapi.py
3
+ This module besapi provides a simple interface to the BigFix REST API.
4
4
 
5
5
  MIT License
6
6
  Copyright (c) 2014 Matt Hansen
@@ -12,30 +12,32 @@ Library for communicating with the BES (BigFix) REST API.
12
12
  import configparser
13
13
  import datetime
14
14
  import hashlib
15
- import io
16
15
  import json
17
16
  import logging
18
17
  import os
19
18
  import random
20
19
  import site
21
20
  import string
21
+ import typing
22
22
 
23
23
  try:
24
24
  from urllib import parse
25
25
  except ImportError:
26
- from urlparse import parse_qs as parse
26
+ from urlparse import parse_qs as parse # type: ignore[no-redef,import-not-found]
27
27
 
28
28
  import requests
29
29
  from lxml import etree, objectify
30
30
  from pkg_resources import resource_filename
31
31
 
32
- __version__ = "3.7.8"
32
+ __version__ = "3.8.2"
33
33
 
34
34
  besapi_logger = logging.getLogger("besapi")
35
35
 
36
+ # pylint: disable=consider-using-f-string
37
+
36
38
 
37
39
  def rand_password(length=20):
38
- """get a random password"""
40
+ """Get a random password."""
39
41
 
40
42
  all_safe_chars = string.ascii_letters + string.digits + "!#()*+,-.:;<=>?[]^_|~"
41
43
 
@@ -66,6 +68,7 @@ def sanitize_txt(*args):
66
68
  def elem2dict(node):
67
69
  """
68
70
  Convert an lxml.etree node tree into a dict.
71
+
69
72
  https://gist.github.com/jacobian/795571?permalink_comment_id=2981870#gistcomment-2981870
70
73
  """
71
74
  result = {}
@@ -94,7 +97,10 @@ def elem2dict(node):
94
97
  def replace_text_between(
95
98
  original_text, first_delimiter, second_delimiter, replacement_text
96
99
  ):
97
- """Replace text between delimeters. Each delimiter should only appear once."""
100
+ """Replace text between delimiters.
101
+
102
+ Each delimiter should only appear once.
103
+ """
98
104
  leading_text = original_text.split(first_delimiter)[0]
99
105
  trailing_text = original_text.split(second_delimiter)[1]
100
106
 
@@ -109,7 +115,7 @@ def replace_text_between(
109
115
 
110
116
  # https://github.com/jgstew/generate_bes_from_template/blob/bcc6c79632dd375c2861608ded3ae5872801a669/src/generate_bes_from_template/generate_bes_from_template.py#L87-L92
111
117
  def parse_bes_modtime(string_datetime):
112
- """parse datetime string to object"""
118
+ """Parse datetime string to object."""
113
119
  # ("%a, %d %b %Y %H:%M:%S %z")
114
120
  return datetime.datetime.strptime(string_datetime, "%a, %d %b %Y %H:%M:%S %z")
115
121
 
@@ -120,7 +126,7 @@ def parse_bes_modtime(string_datetime):
120
126
  # """custom HTTPAdapter for requests to override blocksize
121
127
  # for Uploading or Downloading large files"""
122
128
 
123
- # # override inti_poolmanager from regular HTTPAdapter
129
+ # # override init_poolmanager from regular HTTPAdapter
124
130
  # # https://stackoverflow.com/questions/22915295/python-requests-post-and-big-content/22915488#comment125583017_22915488
125
131
  # def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
126
132
  # """Initializes a urllib3 PoolManager.
@@ -152,8 +158,96 @@ def parse_bes_modtime(string_datetime):
152
158
  # )
153
159
 
154
160
 
161
+ def get_action_combined_relevance(relevances: typing.List[str]):
162
+ """Take array of ordered relevance clauses and return relevance string for
163
+ action.
164
+ """
165
+
166
+ relevance_combined = ""
167
+
168
+ if not relevances:
169
+ return "False"
170
+ if len(relevances) == 0:
171
+ return "False"
172
+ if len(relevances) == 1:
173
+ return relevances[0]
174
+ if len(relevances) > 0:
175
+ for clause in relevances:
176
+ if len(relevance_combined) == 0:
177
+ relevance_combined = clause
178
+ else:
179
+ relevance_combined = (
180
+ "( " + relevance_combined + " ) AND ( " + clause + " )"
181
+ )
182
+
183
+ return relevance_combined
184
+
185
+
186
+ def get_target_xml(targets=None):
187
+ """Get target xml based upon input.
188
+
189
+ Input can be a single string:
190
+ - starts with "<AllComputers>" if all computers should be targeted
191
+ - Otherwise will be interpreted as custom relevance
192
+
193
+ Input can be a single int:
194
+ - Single Computer ID Target
195
+
196
+ Input can be an array:
197
+ - Array of Strings: ComputerName
198
+ - Array of Integers: ComputerID
199
+ """
200
+ if targets is None or not targets:
201
+ logging.warning("No valid targeting found, will target no computers.")
202
+ # default if invalid:
203
+ return "<CustomRelevance>False</CustomRelevance>"
204
+
205
+ # if targets is int:
206
+ if isinstance(targets, int):
207
+ if targets == 0:
208
+ raise ValueError(
209
+ "Int 0 is not valid Computer ID, set targets to an array of strings of computer names or an array of ints of computer ids or custom relevance string or <AllComputers>"
210
+ )
211
+ return f"<ComputerID>{targets}</ComputerID>"
212
+
213
+ # if targets is str:
214
+ if isinstance(targets, str):
215
+ # if targets string starts with "<AllComputers>":
216
+ if targets.startswith("<AllComputers>"):
217
+ if "false" in targets.lower():
218
+ # In my testing, <AllComputers>false</AllComputers> does not work correctly
219
+ return "<CustomRelevance>False</CustomRelevance>"
220
+ # return "<AllComputers>false</AllComputers>"
221
+ return "<AllComputers>true</AllComputers>"
222
+ # treat as custom relevance:
223
+ return f"<CustomRelevance><![CDATA[{targets}]]></CustomRelevance>"
224
+
225
+ # if targets is array:
226
+ if isinstance(targets, list):
227
+ element_type = type(targets[0])
228
+ if element_type is int:
229
+ # array of computer ids
230
+ return (
231
+ "<ComputerID>"
232
+ + "</ComputerID><ComputerID>".join(map(str, targets))
233
+ + "</ComputerID>"
234
+ )
235
+ if element_type is str:
236
+ # array of computer names
237
+ return (
238
+ "<ComputerName>"
239
+ + "</ComputerName><ComputerName>".join(targets)
240
+ + "</ComputerName>"
241
+ )
242
+
243
+ logging.warning("No valid targeting found, will target no computers.")
244
+
245
+ # default if invalid:
246
+ return "<CustomRelevance>False</CustomRelevance>"
247
+
248
+
155
249
  def validate_xsd(doc):
156
- """validate results using XML XSDs"""
250
+ """Validate results using XML XSDs."""
157
251
  try:
158
252
  xmldoc = etree.fromstring(doc)
159
253
  except BaseException: # pylint: disable=broad-except
@@ -176,9 +270,25 @@ def validate_xsd(doc):
176
270
  return False
177
271
 
178
272
 
273
+ def validate_xml_bes_file(file_path):
274
+ """Take a file path as input,
275
+ read as binary data,.
276
+
277
+ validate against xml schema
278
+
279
+ returns True for valid xml
280
+ returns False for invalid xml (or if file is not xml)
281
+ """
282
+ with open(file_path, "rb") as file:
283
+ file_data = file.read()
284
+
285
+ return validate_xsd(file_data)
286
+
287
+
179
288
  def get_bes_conn_using_config_file(conf_file=None):
180
289
  """
181
- read connection values from config file
290
+ Read connection values from config file.
291
+
182
292
  return besapi connection
183
293
  """
184
294
  config_paths = [
@@ -219,7 +329,7 @@ def get_bes_conn_using_config_file(conf_file=None):
219
329
 
220
330
 
221
331
  class BESConnection:
222
- """BigFix RESTAPI connection abstraction class"""
332
+ """BigFix RESTAPI connection abstraction class."""
223
333
 
224
334
  def __init__(self, username, password, rootserver, verify=False):
225
335
  if not verify:
@@ -255,9 +365,9 @@ class BESConnection:
255
365
  self.login()
256
366
 
257
367
  def __repr__(self):
258
- """object representation"""
368
+ """Object representation."""
259
369
  # https://stackoverflow.com/a/2626364/861745
260
- return f"Object: besapi.BESConnction( username={self.username}, rootserver={self.rootserver} )"
370
+ return f"Object: besapi.BESConnection( username={self.username}, rootserver={self.rootserver} )"
261
371
 
262
372
  def __eq__(self, other):
263
373
  if (
@@ -269,16 +379,16 @@ class BESConnection:
269
379
  return False
270
380
 
271
381
  def __del__(self):
272
- """cleanup on deletion of instance"""
382
+ """Cleanup on deletion of instance."""
273
383
  self.logout()
274
384
  self.session.auth = None
275
385
 
276
386
  def __bool__(self):
277
- """get true or false"""
387
+ """Get true or false."""
278
388
  return self.login()
279
389
 
280
390
  def url(self, path):
281
- """get absolute url"""
391
+ """Get absolute url."""
282
392
  if path.startswith(self.rootserver):
283
393
  url = path
284
394
  else:
@@ -287,35 +397,61 @@ class BESConnection:
287
397
  return url
288
398
 
289
399
  def get(self, path="help", **kwargs):
290
- """HTTP GET request"""
400
+ """HTTP GET request."""
291
401
  self.last_connected = datetime.datetime.now()
292
402
  return RESTResult(
293
403
  self.session.get(self.url(path), verify=self.verify, **kwargs)
294
404
  )
295
405
 
296
- def post(self, path, data, **kwargs):
297
- """HTTP POST request"""
406
+ def post(self, path, data, validate_xml=None, **kwargs):
407
+ """HTTP POST request."""
408
+
409
+ # if validate_xml is true, data must validate to xml schema
410
+ # if validate_xml is false, no schema check will be made
411
+ if validate_xml:
412
+ if not validate_xsd(data):
413
+ err_msg = "data being posted did not validate to XML schema. If expected, consider setting validate_xml to false."
414
+ if validate_xml:
415
+ logging.error(err_msg)
416
+ raise ValueError(err_msg)
417
+ else:
418
+ # this is intended it validate_xml is None, but not used currently
419
+ logging.warning(err_msg)
420
+
298
421
  self.last_connected = datetime.datetime.now()
299
422
  return RESTResult(
300
423
  self.session.post(self.url(path), data=data, verify=self.verify, **kwargs)
301
424
  )
302
425
 
303
- def put(self, path, data, **kwargs):
304
- """HTTP PUT request"""
426
+ def put(self, path, data, validate_xml=None, **kwargs):
427
+ """HTTP PUT request."""
305
428
  self.last_connected = datetime.datetime.now()
429
+
430
+ # if validate_xml is true, data must validate to xml schema
431
+ # if validate_xml is false, no schema check will be made
432
+ if validate_xml:
433
+ if not validate_xsd(data):
434
+ err_msg = "data being put did not validate to XML schema. If expected, consider setting validate_xml to false."
435
+ if validate_xml:
436
+ logging.error(err_msg)
437
+ raise ValueError(err_msg)
438
+ else:
439
+ # this is intended it validate_xml is None, but not used currently
440
+ logging.warning(err_msg)
441
+
306
442
  return RESTResult(
307
443
  self.session.put(self.url(path), data=data, verify=self.verify, **kwargs)
308
444
  )
309
445
 
310
446
  def delete(self, path, **kwargs):
311
- """HTTP DELETE request"""
447
+ """HTTP DELETE request."""
312
448
  self.last_connected = datetime.datetime.now()
313
449
  return RESTResult(
314
450
  self.session.delete(self.url(path), verify=self.verify, **kwargs)
315
451
  )
316
452
 
317
453
  def session_relevance_xml(self, relevance, **kwargs):
318
- """Get Session Relevance Results XML"""
454
+ """Get Session Relevance Results XML."""
319
455
  self.last_connected = datetime.datetime.now()
320
456
  return RESTResult(
321
457
  self.session.post(
@@ -327,7 +463,7 @@ class BESConnection:
327
463
  )
328
464
 
329
465
  def session_relevance_array(self, relevance, **kwargs):
330
- """Get Session Relevance Results array"""
466
+ """Get Session Relevance Results array."""
331
467
  rel_result = self.session_relevance_xml(relevance, **kwargs)
332
468
  # print(rel_result)
333
469
  result = []
@@ -354,14 +490,14 @@ class BESConnection:
354
490
  return result
355
491
 
356
492
  def session_relevance_string(self, relevance, **kwargs):
357
- """Get Session Relevance Results string"""
493
+ """Get Session Relevance Results string."""
358
494
  rel_result_array = self.session_relevance_array(
359
495
  "(it as string) of ( " + relevance + " )", **kwargs
360
496
  )
361
497
  return "\n".join(rel_result_array)
362
498
 
363
499
  def login(self):
364
- """do login"""
500
+ """Do login."""
365
501
  if bool(self.last_connected):
366
502
  duration_obj = datetime.datetime.now() - self.last_connected
367
503
  duration_minutes = duration_obj / datetime.timedelta(minutes=1)
@@ -392,14 +528,14 @@ class BESConnection:
392
528
  return bool(self.last_connected)
393
529
 
394
530
  def logout(self):
395
- """clear session and close it"""
531
+ """Clear session and close it."""
396
532
  self.session.cookies.clear()
397
533
  self.session.close()
398
534
 
399
535
  def set_dashboard_variable_value(
400
536
  self, dashboard_name, var_name, var_value, private=False
401
537
  ):
402
- """set the variable value from a dashboard datastore"""
538
+ """Set the variable value from a dashboard datastore."""
403
539
 
404
540
  dash_var_xml = f"""<BESAPI xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="BESAPI.xsd">
405
541
  <DashboardData>
@@ -416,7 +552,7 @@ class BESConnection:
416
552
  )
417
553
 
418
554
  def get_dashboard_variable_value(self, dashboard_name, var_name):
419
- """get the variable value from a dashboard datastore"""
555
+ """Get the variable value from a dashboard datastore."""
420
556
 
421
557
  return str(
422
558
  self.get(
@@ -425,7 +561,7 @@ class BESConnection:
425
561
  )
426
562
 
427
563
  def validate_site_path(self, site_path, check_site_exists=True, raise_error=False):
428
- """make sure site_path is valid"""
564
+ """Make sure site_path is valid."""
429
565
 
430
566
  if site_path is None:
431
567
  if not raise_error:
@@ -468,8 +604,9 @@ class BESConnection:
468
604
  )
469
605
 
470
606
  def get_current_site_path(self, site_path=None):
471
- """if site_path is none, get current instance site_path,
472
- otherwise validate and return provided site_path"""
607
+ """If site_path is none, get current instance site_path,
608
+ otherwise validate and return provided site_path.
609
+ """
473
610
 
474
611
  # use instance site_path context if none provided:
475
612
  if site_path is None or str(site_path).strip() == "":
@@ -483,14 +620,14 @@ class BESConnection:
483
620
  return self.validate_site_path(site_path, check_site_exists=False)
484
621
 
485
622
  def set_current_site_path(self, site_path):
486
- """set current site path context"""
623
+ """Set current site path context."""
487
624
 
488
625
  if self.validate_site_path(site_path):
489
626
  self.site_path = site_path
490
627
  return self.site_path
491
628
 
492
629
  def import_bes_to_site(self, bes_file_path, site_path=None):
493
- """import bes file to site"""
630
+ """Import bes file to site."""
494
631
 
495
632
  if not os.access(bes_file_path, os.R_OK):
496
633
  besapi_logger.error("%s is not readable", bes_file_path)
@@ -513,7 +650,7 @@ class BESConnection:
513
650
  return result
514
651
 
515
652
  def create_site_from_file(self, bes_file_path, site_type="custom"):
516
- """create new site"""
653
+ """Create new site."""
517
654
  xml_parsed = etree.parse(bes_file_path)
518
655
  new_site_name = xml_parsed.xpath("/BES/CustomSite/Name/text()")[0]
519
656
 
@@ -530,7 +667,7 @@ class BESConnection:
530
667
  return result_site
531
668
 
532
669
  def get_user(self, user_name):
533
- """get a user"""
670
+ """Get a user."""
534
671
 
535
672
  result_users = self.get(f"operator/{user_name}")
536
673
 
@@ -540,7 +677,7 @@ class BESConnection:
540
677
  besapi_logger.info("User `%s` Not Found!", user_name)
541
678
 
542
679
  def create_user_from_file(self, bes_file_path):
543
- """create user from xml"""
680
+ """Create user from xml."""
544
681
  xml_parsed = etree.parse(bes_file_path)
545
682
  new_user_name = xml_parsed.xpath("/BESAPI/Operator/Name/text()")[0]
546
683
  result_user = self.get_user(new_user_name)
@@ -554,7 +691,7 @@ class BESConnection:
554
691
  return self.get_user(new_user_name)
555
692
 
556
693
  def get_computergroup(self, group_name, site_path=None):
557
- """get computer group resource URI"""
694
+ """Get computer group resource URI."""
558
695
 
559
696
  site_path = self.get_current_site_path(site_path)
560
697
  result_groups = self.get(f"computergroups/{site_path}")
@@ -569,7 +706,7 @@ class BESConnection:
569
706
  besapi_logger.info("Group `%s` Not Found!", group_name)
570
707
 
571
708
  def create_group_from_file(self, bes_file_path, site_path=None):
572
- """create a new group"""
709
+ """Create a new group."""
573
710
  site_path = self.get_current_site_path(site_path)
574
711
  xml_parsed = etree.parse(bes_file_path)
575
712
  new_group_name = xml_parsed.xpath("/BES/ComputerGroup/Title/text()")[0]
@@ -588,7 +725,7 @@ class BESConnection:
588
725
 
589
726
  def get_upload(self, file_name, file_hash):
590
727
  """
591
- check for a specific file upload reference
728
+ Check for a specific file upload reference.
592
729
 
593
730
  each upload is uniquely identified by sha1 and filename
594
731
 
@@ -614,7 +751,8 @@ class BESConnection:
614
751
 
615
752
  def upload(self, file_path, file_name=None, file_hash=None):
616
753
  """
617
- upload a single file
754
+ Upload a single file.
755
+
618
756
  https://developer.bigfix.com/rest-api/api/upload.html
619
757
  """
620
758
  if not os.access(file_path, os.R_OK):
@@ -665,11 +803,11 @@ class BESConnection:
665
803
  def parse_upload_result_to_prefetch(
666
804
  self, result_upload, use_localhost=True, use_https=True
667
805
  ):
668
- """take a rest response from an upload and parse into prefetch"""
806
+ """Take a rest response from an upload and parse into prefetch."""
669
807
  file_url = str(result_upload.besobj.FileUpload.URL)
670
808
  if use_https:
671
809
  file_url = file_url.replace("http://", "https://")
672
- # there are 3 different posibilities for the server FQDN
810
+ # there are 3 different possibilities for the server FQDN
673
811
  # localhost
674
812
  # self.rootserver (without port number)
675
813
  # the returned value from the upload result
@@ -686,7 +824,7 @@ class BESConnection:
686
824
  return f"prefetch {file_name} sha1:{file_sha1} size:{file_size} {file_url} sha256:{file_sha256}"
687
825
 
688
826
  def get_content_by_resource(self, resource_url):
689
- """get a single content item by resource"""
827
+ """Get a single content item by resource."""
690
828
  # Get Specific Content
691
829
  content = None
692
830
  try:
@@ -704,7 +842,7 @@ class BESConnection:
704
842
  return content
705
843
 
706
844
  def update_item_from_file(self, file_path, site_path=None):
707
- """update an item by name and last modified"""
845
+ """Update an item by name and last modified."""
708
846
  site_path = self.get_current_site_path(site_path)
709
847
  bes_tree = etree.parse(file_path)
710
848
 
@@ -737,7 +875,7 @@ class BESConnection:
737
875
  export_folder="./",
738
876
  name_trim=100,
739
877
  ):
740
- """save an xml string to bes file"""
878
+ """Save an xml string to bes file."""
741
879
  item_folder = export_folder
742
880
  if not os.path.exists(item_folder):
743
881
  os.makedirs(item_folder)
@@ -765,8 +903,10 @@ class BESConnection:
765
903
  include_item_type_folder=False,
766
904
  include_item_id=False,
767
905
  ):
768
- """export a single item by resource
906
+ """Export a single item by resource.
907
+
769
908
  example resources:
909
+
770
910
  - content_type/site_type/site/id
771
911
  - https://localhost:52311/api/content_type/site_type/site/id
772
912
  """
@@ -813,8 +953,10 @@ class BESConnection:
813
953
  include_site_folder=True,
814
954
  include_item_ids=True,
815
955
  ):
816
- """export contents of site
956
+ """Export contents of site.
957
+
817
958
  Originally here:
959
+
818
960
  - https://gist.github.com/jgstew/1b2da12af59b71c9f88a
819
961
  - https://bigfix.me/fixlet/details/21282
820
962
  """
@@ -890,7 +1032,7 @@ class BESConnection:
890
1032
  def export_all_sites(
891
1033
  self, include_external=False, export_folder="./", name_trim=70, verbose=False
892
1034
  ):
893
- """export all bigfix sites to a folder"""
1035
+ """Export all bigfix sites to a folder."""
894
1036
  results_sites = self.get("sites")
895
1037
  if verbose:
896
1038
  print(results_sites)
@@ -909,11 +1051,12 @@ class BESConnection:
909
1051
 
910
1052
 
911
1053
  class RESTResult:
912
- """BigFix REST API Result Abstraction Class"""
1054
+ """BigFix REST API Result Abstraction Class."""
913
1055
 
914
1056
  def __init__(self, request):
915
1057
  self.request = request
916
1058
  self.text = request.text
1059
+ self.valid = None
917
1060
  self._besxml = None
918
1061
  self._besobj = None
919
1062
  self._besdict = None
@@ -927,7 +1070,7 @@ class RESTResult:
927
1070
  f"\n - HTTP Response Status Code: `403` Forbidden\n - ERROR: `{self.text}`\n - URL: `{self.request.url}`"
928
1071
  )
929
1072
 
930
- besapi_logger.info(
1073
+ besapi_logger.debug(
931
1074
  "HTTP Request Status Code `%d` from URL `%s`",
932
1075
  self.request.status_code,
933
1076
  self.request.url,
@@ -948,7 +1091,9 @@ class RESTResult:
948
1091
  if self.validate_xsd(request.text):
949
1092
  self.valid = True
950
1093
  else:
951
- # print("WARNING: response appears invalid")
1094
+ logging.debug(
1095
+ "INFO: REST API Result does not appear to be XML, this could be expected."
1096
+ )
952
1097
  self.valid = False
953
1098
 
954
1099
  def __str__(self):
@@ -966,7 +1111,7 @@ class RESTResult:
966
1111
 
967
1112
  @property
968
1113
  def besxml(self):
969
- """property for parsed xml representation"""
1114
+ """Property for parsed xml representation."""
970
1115
  if self.valid and self._besxml is None:
971
1116
  self._besxml = self.xmlparse_text(self.text)
972
1117
 
@@ -974,7 +1119,7 @@ class RESTResult:
974
1119
 
975
1120
  @property
976
1121
  def besobj(self):
977
- """property for xml object representation"""
1122
+ """Property for xml object representation."""
978
1123
  if self.valid and self._besobj is None:
979
1124
  self._besobj = self.objectify_text(self.text)
980
1125
 
@@ -982,7 +1127,7 @@ class RESTResult:
982
1127
 
983
1128
  @property
984
1129
  def besdict(self):
985
- """property for python dict representation"""
1130
+ """Property for python dict representation."""
986
1131
  if self._besdict is None:
987
1132
  if self.valid:
988
1133
  self._besdict = elem2dict(etree.fromstring(self.besxml))
@@ -993,18 +1138,21 @@ class RESTResult:
993
1138
 
994
1139
  @property
995
1140
  def besjson(self):
996
- """property for json representation"""
1141
+ """Property for json representation."""
997
1142
  if self._besjson is None:
998
1143
  self._besjson = json.dumps(self.besdict, indent=2)
999
1144
 
1000
1145
  return self._besjson
1001
1146
 
1002
1147
  def validate_xsd(self, doc):
1003
- """validate results using XML XSDs"""
1148
+ """Validate results using XML XSDs."""
1149
+ # return self.valid if already set
1150
+ if self.valid is not None and isinstance(self.valid, bool):
1151
+ return self.valid
1004
1152
  return validate_xsd(doc)
1005
1153
 
1006
1154
  def xmlparse_text(self, text):
1007
- """parse response text as xml"""
1155
+ """Parse response text as xml."""
1008
1156
  if type(text) is str:
1009
1157
  root_xml = etree.fromstring(text.encode("utf-8"))
1010
1158
  else:
@@ -1013,7 +1161,7 @@ class RESTResult:
1013
1161
  return etree.tostring(root_xml, encoding="utf-8", xml_declaration=True)
1014
1162
 
1015
1163
  def objectify_text(self, text):
1016
- """parse response text as objectified xml"""
1164
+ """Parse response text as objectified xml."""
1017
1165
  if type(text) is str:
1018
1166
  root_xml = text.encode("utf-8")
1019
1167
  else:
@@ -1023,7 +1171,7 @@ class RESTResult:
1023
1171
 
1024
1172
 
1025
1173
  def main():
1026
- """if invoked directly, run bescli command loop"""
1174
+ """If invoked directly, run bescli command loop."""
1027
1175
  # pylint: disable=import-outside-toplevel
1028
1176
  try:
1029
1177
  from bescli import bescli
@@ -1,4 +1,4 @@
1
- """This is a set of utility functions for use in multiple plugins
1
+ """This is a set of utility functions for use in multiple plugins.
2
2
 
3
3
  see example here: https://github.com/jgstew/besapi/blob/master/examples/export_all_sites.py
4
4
  """
@@ -16,7 +16,7 @@ import besapi
16
16
 
17
17
  # NOTE: This does not work as expected when run from plugin_utilities
18
18
  def get_invoke_folder(verbose=0):
19
- """Get the folder the script was invoked from"""
19
+ """Get the folder the script was invoked from."""
20
20
  # using logging here won't actually log it to the file:
21
21
 
22
22
  if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
@@ -36,7 +36,7 @@ def get_invoke_folder(verbose=0):
36
36
 
37
37
  # NOTE: This does not work as expected when run from plugin_utilities
38
38
  def get_invoke_file_name(verbose=0):
39
- """Get the filename the script was invoked from"""
39
+ """Get the filename the script was invoked from."""
40
40
  # using logging here won't actually log it to the file:
41
41
 
42
42
  if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
@@ -56,9 +56,9 @@ def get_invoke_file_name(verbose=0):
56
56
 
57
57
 
58
58
  def setup_plugin_argparse(plugin_args_required=False):
59
- """setup argparse for plugin use"""
59
+ """Setup argparse for plugin use."""
60
60
  arg_parser = argparse.ArgumentParser(
61
- description="Provde command line arguments for REST URL, username, and password"
61
+ description="Provide command line arguments for REST URL, username, and password"
62
62
  )
63
63
  arg_parser.add_argument(
64
64
  "-v",
@@ -92,9 +92,10 @@ def setup_plugin_argparse(plugin_args_required=False):
92
92
 
93
93
 
94
94
  def get_plugin_logging_config(log_file_path="", verbose=0, console=True):
95
- """get config for logging for plugin use
95
+ """Get config for logging for plugin use.
96
96
 
97
- use this like: logging.basicConfig(**logging_config)"""
97
+ use this like: logging.basicConfig(**logging_config)
98
+ """
98
99
 
99
100
  if not log_file_path or log_file_path == "":
100
101
  log_file_path = os.path.join(
@@ -131,7 +132,9 @@ def get_plugin_logging_config(log_file_path="", verbose=0, console=True):
131
132
 
132
133
 
133
134
  def get_besapi_connection(args):
134
- """get connection to besapi using either args or config file if args not provided"""
135
+ """Get connection to besapi using either args or config file if args not
136
+ provided.
137
+ """
135
138
 
136
139
  password = args.password
137
140
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: besapi
3
- Version: 3.7.8
3
+ Version: 3.8.2
4
4
  Summary: Library for working with the BigFix REST API
5
5
  Home-page: https://github.com/jgstew/besapi
6
6
  Author: Matt Hansen, James Stewart
@@ -8,14 +8,27 @@ Author-email: hansen.m@psu.edu, james@jgstew.com
8
8
  License: MIT
9
9
  Keywords: bigfix iem tem rest api
10
10
  Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
11
12
  Classifier: Operating System :: OS Independent
12
13
  Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Systems Administration
13
18
  Requires-Python: >=3.7
14
19
  Description-Content-Type: text/markdown
15
20
  License-File: LICENSE.txt
16
21
  Requires-Dist: requests
17
22
  Requires-Dist: lxml
18
23
  Requires-Dist: cmd2
24
+ Requires-Dist: setuptools
25
+ Dynamic: author
26
+ Dynamic: author-email
27
+ Dynamic: home-page
28
+ Dynamic: keywords
29
+ Dynamic: license
30
+ Dynamic: requires-dist
31
+ Dynamic: summary
19
32
 
20
33
  # besapi
21
34
 
@@ -1,3 +1,4 @@
1
1
  requests
2
2
  lxml
3
3
  cmd2
4
+ setuptools
@@ -1,6 +1,4 @@
1
- """
2
- bescli provides a command line interface to interact with besapi
3
- """
1
+ """This bescli provides a command line interface to interact with besapi."""
4
2
 
5
3
  # https://stackoverflow.com/questions/279237/import-a-module-from-a-relative-path/4397291
6
4
 
@@ -1,6 +1,4 @@
1
- """
2
- To run this module directly
3
- """
1
+ """To run this module directly."""
4
2
 
5
3
  import logging
6
4
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python
2
2
  """
3
- bescli.py
3
+ This bescli module provides a command line interface to interact with besapi.
4
4
 
5
5
  MIT License
6
6
  Copyright (c) 2014 Matt Hansen
@@ -27,12 +27,12 @@ except ModuleNotFoundError:
27
27
  from besapi import besapi
28
28
  except ImportError:
29
29
  # this is for the case in which we are calling bescli from besapi
30
- import besapi
30
+ import besapi # type: ignore[no-redef]
31
31
 
32
32
  try:
33
33
  from besapi.besapi import __version__
34
34
  except ImportError:
35
- from besapi import __version__
35
+ from besapi import __version__ # type: ignore
36
36
 
37
37
 
38
38
  class BESCLInterface(Cmd):
@@ -62,7 +62,7 @@ class BESCLInterface(Cmd):
62
62
  self.do_conf()
63
63
 
64
64
  def parse_help_resources(self):
65
- """get api resources from help"""
65
+ """Get api resources from help."""
66
66
  if self.bes_conn:
67
67
  help_result = self.bes_conn.get("help")
68
68
  help_result = help_result.text.split("\n")
@@ -89,7 +89,7 @@ class BESCLInterface(Cmd):
89
89
  ]
90
90
 
91
91
  def complete_api_resources(self, text, line, begidx, endidx):
92
- """define completion for apis"""
92
+ """Define completion for apis."""
93
93
 
94
94
  # only initialize once
95
95
  if not self.api_resources:
@@ -102,7 +102,9 @@ class BESCLInterface(Cmd):
102
102
  complete_get = complete_api_resources
103
103
 
104
104
  def do_get(self, line):
105
- """Perform get request to BigFix server using provided api endpoint argument"""
105
+ """Perform get request to BigFix server using provided api endpoint
106
+ argument.
107
+ """
106
108
 
107
109
  # remove any extra whitespace
108
110
  line = line.strip()
@@ -136,7 +138,9 @@ class BESCLInterface(Cmd):
136
138
  complete_delete = complete_api_resources
137
139
 
138
140
  def do_delete(self, line):
139
- """Perform delete request to BigFix server using provided api endpoint argument"""
141
+ """Perform delete request to BigFix server using provided api endpoint
142
+ argument.
143
+ """
140
144
 
141
145
  # remove any extra whitespace
142
146
  line = line.strip()
@@ -158,20 +162,20 @@ class BESCLInterface(Cmd):
158
162
  complete_post = complete_api_resources
159
163
 
160
164
  def do_post(self, statement):
161
- """post file as data to path"""
165
+ """Post file as data to path."""
162
166
  self.poutput(statement)
163
167
  self.poutput("not yet implemented")
164
168
 
165
169
  def do_config(self, conf_file=None):
166
- """Attempt to load config info from file and login"""
170
+ """Attempt to load config info from file and login."""
167
171
  self.do_conf(conf_file)
168
172
 
169
173
  def do_loadconfig(self, conf_file=None):
170
- """Attempt to load config info from file and login"""
174
+ """Attempt to load config info from file and login."""
171
175
  self.do_conf(conf_file)
172
176
 
173
177
  def do_conf(self, conf_file=None):
174
- """Attempt to load config info from file and login"""
178
+ """Attempt to load config info from file and login."""
175
179
  config_path = [
176
180
  "/etc/besapi.conf",
177
181
  os.path.expanduser("~/besapi.conf"),
@@ -213,7 +217,7 @@ class BESCLInterface(Cmd):
213
217
  self.do_login()
214
218
 
215
219
  def do_login(self, user=None):
216
- """Login to BigFix Server"""
220
+ """Login to BigFix Server."""
217
221
 
218
222
  if not user:
219
223
  if self.BES_USER_NAME:
@@ -292,7 +296,7 @@ class BESCLInterface(Cmd):
292
296
  self.perror("Login Error!")
293
297
 
294
298
  def do_logout(self, _=None):
295
- """Logout and clear session"""
299
+ """Logout and clear session."""
296
300
  if self.bes_conn:
297
301
  self.bes_conn.logout()
298
302
  # del self.bes_conn
@@ -300,7 +304,7 @@ class BESCLInterface(Cmd):
300
304
  self.pfeedback("Logout Complete!")
301
305
 
302
306
  def do_debug(self, setting):
303
- """Enable or Disable Debug Mode"""
307
+ """Enable or Disable Debug Mode."""
304
308
  self.poutput(bool(setting))
305
309
  self.debug = bool(setting)
306
310
  self.echo = bool(setting)
@@ -312,7 +316,7 @@ class BESCLInterface(Cmd):
312
316
  logging.getLogger("besapi").setLevel(logging.WARNING)
313
317
 
314
318
  def do_clear(self, arg=None):
315
- """clear current config and logout"""
319
+ """Clear current config and logout."""
316
320
  if self.bes_conn:
317
321
  self.bes_conn.logout()
318
322
  # self.bes_conn = None
@@ -332,11 +336,11 @@ class BESCLInterface(Cmd):
332
336
  self.BES_PASSWORD = None
333
337
 
334
338
  def do_saveconfig(self, arg=None):
335
- """save current config to file"""
339
+ """Save current config to file."""
336
340
  self.do_saveconf(arg)
337
341
 
338
342
  def do_saveconf(self, _=None):
339
- """save current config to file"""
343
+ """Save current config to file."""
340
344
  if not self.bes_conn:
341
345
  self.do_login()
342
346
  if not self.bes_conn:
@@ -348,11 +352,11 @@ class BESCLInterface(Cmd):
348
352
  self.CONFPARSER.write(configfile)
349
353
 
350
354
  def do_showconfig(self, arg=None):
351
- """List the current settings and connection status"""
355
+ """List the current settings and connection status."""
352
356
  self.do_ls(arg)
353
357
 
354
358
  def do_ls(self, _=None):
355
- """List the current settings and connection status"""
359
+ """List the current settings and connection status."""
356
360
  self.poutput(" Connected: " + str(bool(self.bes_conn)))
357
361
  self.poutput(
358
362
  " BES_ROOT_SERVER: "
@@ -372,17 +376,17 @@ class BESCLInterface(Cmd):
372
376
  )
373
377
 
374
378
  def do_error_count(self, _=None):
375
- """Output the number of errors"""
379
+ """Output the number of errors."""
376
380
  self.poutput(f"Error Count: {self.num_errors}")
377
381
 
378
382
  def do_exit(self, _=None):
379
- """Exit this application"""
383
+ """Exit this application."""
380
384
  self.exit_code = self.num_errors
381
385
  # no matter what I try I can't get anything but exit code 0 on windows
382
386
  return self.do_quit("")
383
387
 
384
388
  def do_query(self, statement):
385
- """Get Session Relevance Results"""
389
+ """Get Session Relevance Results."""
386
390
  if not self.bes_conn:
387
391
  self.do_login()
388
392
  if not self.bes_conn:
@@ -397,53 +401,53 @@ class BESCLInterface(Cmd):
397
401
  self.poutput(rel_result)
398
402
 
399
403
  def do_version(self, _=None):
400
- """output version of besapi"""
404
+ """Output version of besapi."""
401
405
  self.poutput(f"besapi version: {__version__}")
402
406
 
403
407
  def do_get_action(self, statement=None):
404
- """usage: get_action 123"""
408
+ """Usage: get_action 123."""
405
409
  result_op = self.bes_conn.get(f"action/{statement}")
406
410
  self.poutput(result_op)
407
411
 
408
412
  def do_get_operator(self, statement=None):
409
- """usage: get_operator ExampleOperatorName"""
413
+ """Usage: get_operator ExampleOperatorName."""
410
414
  result_op = self.bes_conn.get_user(statement)
411
415
  self.poutput(result_op)
412
416
 
413
417
  def do_get_current_site(self, _=None):
414
- """output current site path context"""
418
+ """Output current site path context."""
415
419
  self.poutput(
416
- f"Current Site Path: `{ self.bes_conn.get_current_site_path(None) }`"
420
+ f"Current Site Path: `{self.bes_conn.get_current_site_path(None)}`"
417
421
  )
418
422
 
419
423
  def do_set_current_site(self, statement=None):
420
- """set current site path context"""
424
+ """Set current site path context."""
421
425
  self.poutput(
422
- f"New Site Path: `{ self.bes_conn.set_current_site_path(statement) }`"
426
+ f"New Site Path: `{self.bes_conn.set_current_site_path(statement)}`"
423
427
  )
424
428
 
425
429
  def do_get_content(self, resource_url):
426
- """get a specific item by resource url"""
430
+ """Get a specific item by resource url."""
427
431
  self.poutput(self.bes_conn.get_content_by_resource(resource_url))
428
432
 
429
433
  def do_export_item_by_resource(self, statement):
430
- """export content itemb to current folder"""
434
+ """Export content itemb to current folder."""
431
435
  self.poutput(self.bes_conn.export_item_by_resource(statement))
432
436
 
433
437
  def do_export_site(self, site_path):
434
- """export site contents to current folder"""
438
+ """Export site contents to current folder."""
435
439
  self.bes_conn.export_site_contents(
436
440
  site_path, verbose=True, include_site_folder=False, include_item_ids=False
437
441
  )
438
442
 
439
443
  def do_export_all_sites(self, _=None):
440
- """export site contents to current folder"""
444
+ """Export site contents to current folder."""
441
445
  self.bes_conn.export_all_sites(verbose=False)
442
446
 
443
447
  complete_import_bes = Cmd.path_complete
444
448
 
445
449
  def do_import_bes(self, statement):
446
- """import bes file"""
450
+ """Import bes file."""
447
451
 
448
452
  bes_file_path = str(statement.args).strip()
449
453
 
@@ -456,7 +460,7 @@ class BESCLInterface(Cmd):
456
460
  complete_upload = Cmd.path_complete
457
461
 
458
462
  def do_upload(self, file_path):
459
- """upload file to root server"""
463
+ """Upload file to root server."""
460
464
  if not os.access(file_path, os.R_OK):
461
465
  self.poutput(file_path, "is not a readable file")
462
466
  else:
@@ -467,7 +471,7 @@ class BESCLInterface(Cmd):
467
471
  complete_create_group = Cmd.path_complete
468
472
 
469
473
  def do_create_group(self, file_path):
470
- """create bigfix group from bes file"""
474
+ """Create bigfix group from bes file."""
471
475
  if not os.access(file_path, os.R_OK):
472
476
  self.poutput(file_path, "is not a readable file")
473
477
  else:
@@ -476,7 +480,7 @@ class BESCLInterface(Cmd):
476
480
  complete_create_user = Cmd.path_complete
477
481
 
478
482
  def do_create_user(self, file_path):
479
- """create bigfix user from bes file"""
483
+ """Create bigfix user from bes file."""
480
484
  if not os.access(file_path, os.R_OK):
481
485
  self.poutput(file_path, "is not a readable file")
482
486
  else:
@@ -485,7 +489,7 @@ class BESCLInterface(Cmd):
485
489
  complete_create_site = Cmd.path_complete
486
490
 
487
491
  def do_create_site(self, file_path):
488
- """create bigfix site from bes file"""
492
+ """Create bigfix site from bes file."""
489
493
  if not os.access(file_path, os.R_OK):
490
494
  self.poutput(file_path, "is not a readable file")
491
495
  else:
@@ -494,14 +498,14 @@ class BESCLInterface(Cmd):
494
498
  complete_update_item = Cmd.path_complete
495
499
 
496
500
  def do_update_item(self, file_path):
497
- """update bigfix content item from bes file"""
501
+ """Update bigfix content item from bes file."""
498
502
  if not os.access(file_path, os.R_OK):
499
503
  self.poutput(file_path, "is not a readable file")
500
504
  else:
501
505
  self.poutput(self.bes_conn.update_item_from_file(file_path))
502
506
 
503
507
  def do_serverinfo(self, _=None):
504
- """get server info and return formatted"""
508
+ """Get server info and return formatted."""
505
509
 
506
510
  # not sure what the minimum version for this is:
507
511
  result = self.bes_conn.get("serverinfo")
@@ -513,7 +517,7 @@ class BESCLInterface(Cmd):
513
517
 
514
518
 
515
519
  def main():
516
- """Run the command loop if invoked"""
520
+ """Run the command loop if invoked."""
517
521
  BESCLInterface().cmdloop()
518
522
 
519
523
 
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env python
2
- """
3
- Test besapi
4
- """
2
+ """Test besapi."""
5
3
 
6
4
  import argparse
7
5
  import os
@@ -9,6 +7,8 @@ import random
9
7
  import subprocess
10
8
  import sys
11
9
 
10
+ # mypy: disable-error-code="arg-type"
11
+
12
12
  # check for --test_pip arg
13
13
  parser = argparse.ArgumentParser()
14
14
  parser.add_argument(
@@ -79,7 +79,7 @@ assert raised_errors == 4
79
79
 
80
80
  class RequestResult(object):
81
81
  text = "this is just a test"
82
- headers = []
82
+ headers = [] # type: ignore
83
83
 
84
84
 
85
85
  request_result = RequestResult()
@@ -91,6 +91,37 @@ assert b"<BES>Example</BES>" in rest_result.xmlparse_text("<BES>Example</BES>")
91
91
 
92
92
  assert rest_result.text == "this is just a test"
93
93
 
94
+ # test date parsing function:
95
+ assert 2017 == besapi.besapi.parse_bes_modtime("Tue, 05 Sep 2017 23:31:48 +0000").year
96
+
97
+ # test action combined relevance
98
+ assert (
99
+ "( ( True ) AND ( windows of operating system ) ) AND ( False )"
100
+ == besapi.besapi.get_action_combined_relevance(
101
+ ["True", "windows of operating system", "False"]
102
+ )
103
+ )
104
+
105
+ # test target xml
106
+ assert "<CustomRelevance>False</CustomRelevance>" == besapi.besapi.get_target_xml()
107
+ assert "<AllComputers>true</AllComputers>" == besapi.besapi.get_target_xml(
108
+ "<AllComputers>"
109
+ )
110
+ assert "<ComputerID>1</ComputerID>" == besapi.besapi.get_target_xml(1)
111
+ assert (
112
+ "<CustomRelevance><![CDATA[not windows of operating system]]></CustomRelevance>"
113
+ == besapi.besapi.get_target_xml("not windows of operating system")
114
+ )
115
+ assert (
116
+ "<ComputerID>1</ComputerID><ComputerID>2</ComputerID>"
117
+ == besapi.besapi.get_target_xml([1, 2])
118
+ )
119
+ assert (
120
+ "<ComputerName>Computer 1</ComputerName><ComputerName>Another Computer</ComputerName>"
121
+ == besapi.besapi.get_target_xml(["Computer 1", "Another Computer"])
122
+ )
123
+
124
+ # test bescli:
94
125
  import bescli
95
126
 
96
127
  bigfix_cli = bescli.bescli.BESCLInterface()
@@ -151,8 +182,10 @@ if bigfix_cli.bes_conn:
151
182
  'CMD /C python -m besapi ls clear ls conf "query number of bes computers" version error_count exit',
152
183
  check=True,
153
184
  )
154
- bes_conn = besapi.besapi.get_bes_conn_using_config_file()
155
- print("login succeeded:", bes_conn.login())
185
+
186
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
187
+ print("login succeeded:", bes_conn.login())
188
+ assert bes_conn.login()
156
189
 
157
190
  # test plugin_utilities:
158
191
  print(besapi.plugin_utilities.get_invoke_folder())
File without changes
File without changes
File without changes
File without changes