110110class IncomparableException (Exception ):
111111 pass
112112
113+ class JobNotReadyException (Exception ):
114+ pass
115+
116+ class AmbiguousReferenceException (ValueError ):
117+ pass
118+
119+ def trailing (template , * targets ):
120+ """Substring of *template* following all *targets*.
121+
122+ Most easily explained by example::
123+
124+ template = "this is a test of the bunnies."
125+ trailing(template, "is", "est", "the") == \
126+ " bunnies"
127+
128+ Each target is matched successively in the string, and the string
129+ remaining after the last target is returned. If one of the targets
130+ fails to match, a ValueError is raised.
131+
132+ :param template: Template to extract a trailing string from.
133+ :type template: string
134+ :param targets: Strings to successively match in *template*.
135+ :type targets: strings
136+ :returns: Trailing string after all targets are matched.
137+ :rtype: string
138+ :raises ValueError: when one of the targets does not match.
139+ """
140+ s = template
141+ for t in targets :
142+ n = s .find (t )
143+ if n == - 1 :
144+ raise ValueError ("Target " + t + " not found in template." )
145+ s = s [n + len (t ):]
146+ return s
147+
113148# Filter the given state content record according to the given arg list.
114149def _filter_content (content , * args ):
115150 if len (args ) > 0 :
@@ -458,7 +493,8 @@ def get(self, path_segment="", owner=None, app=None, sharing=None, **query):
458493 if path_segment .startswith ('/' ):
459494 path = path_segment
460495 else :
461- path = self .path + path_segment
496+ path = self .service ._abspath (self .path + path_segment , owner = owner ,
497+ app = app , sharing = sharing )
462498 # ^-- This was "%s%s" % (self.path, path_segment).
463499 # That doesn't work, because self.path may be UrlEncoded.
464500 return self .service .get (path ,
@@ -513,7 +549,8 @@ def post(self, path_segment="", owner=None, app=None, sharing=None, **query):
513549 if path_segment .startswith ('/' ):
514550 path = path_segment
515551 else :
516- path = self .path + path_segment
552+ path = self .service ._abspath (self .path + path_segment , owner = owner ,
553+ app = app , sharing = sharing )
517554 return self .service .post (path ,
518555 owner = owner , app = app , sharing = sharing ,
519556 ** query )
@@ -584,7 +621,8 @@ class Entity(Endpoint):
584621 def __init__ (self , service , path , ** kwargs ):
585622 Endpoint .__init__ (self , service , path )
586623 self ._state = None
587- self .refresh (kwargs .get ('state' , None )) # "Prefresh"
624+ if not kwargs .get ('skip_refresh' , False ):
625+ self .refresh (kwargs .get ('state' , None )) # "Prefresh"
588626
589627 def __eq__ (self , other ):
590628 """Raises IncomparableException.
@@ -844,7 +882,6 @@ def __init__(self, service, path, item=Entity):
844882 self .item = item # Item accessor
845883 self .null_count = - 1
846884
847-
848885 def __contains__ (self , name ):
849886 """Is there at least one entry called *name* in this collection?
850887
@@ -856,6 +893,8 @@ def __contains__(self, name):
856893 return True
857894 except KeyError :
858895 return False
896+ except AmbiguousReferenceException :
897+ return True
859898
860899 def __getitem__ (self , key ):
861900 """Fetch an item named *key* from this collection.
@@ -915,7 +954,9 @@ def __getitem__(self, key):
915954 response = self .get (key , owner = ns .owner , app = ns .app )
916955 entries = self ._load_list (response )
917956 if len (entries ) > 1 :
918- raise ValueError ("Found multiple entities named '%s'; please specify a namespace." % key )
957+ raise AmbiguousReferenceException ("Found multiple entities named '%s'; please specify a namespace." % key )
958+ elif len (entries ) == 0 :
959+ raise KeyError (key )
919960 else :
920961 return entries [0 ]
921962 except HTTPError as he :
@@ -968,15 +1009,24 @@ def _entity_path(self, state):
9681009 """Calculate the path to an entity to be returned.
9691010
9701011 *state* should be the dictionary returned by
971- :func:`_parse_atom_entry`.
1012+ :func:`_parse_atom_entry`. :func:`_entity_path` extracts the
1013+ link to this entity from *state*, and strips all the namespace
1014+ prefixes from it to leave only the relative path of the entity
1015+ itself, sans namespace.
9721016
9731017 :rtype: string
9741018 :returns: an absolute path
9751019 """
9761020 # This has been factored out so that it can be easily
9771021 # overloaded by Configurations, which has to switch its
9781022 # entities' endpoints from its own properties/ to configs/.
979- return urllib .unquote (state .links .alternate )
1023+ raw_path = urllib .unquote (state .links .alternate )
1024+ if 'servicesNS/' in raw_path :
1025+ return trailing (raw_path , 'servicesNS/' , '/' , '/' )
1026+ elif 'services/' in raw_path :
1027+ return trailing (raw_path , 'services/' )
1028+ else :
1029+ return raw_path
9801030
9811031 def _load_list (self , response ):
9821032 """Converts *response* to a list of entities.
@@ -1070,7 +1120,7 @@ def create(self, name, **params):
10701120 state = _parse_atom_entry (entry )
10711121 entity = self .item (
10721122 self .service ,
1073- urllib . unquote (state . links . alternate ),
1123+ self . _entity_path (state ),
10741124 state = state )
10751125 return entity
10761126
@@ -1268,7 +1318,7 @@ def _entity_path(self, state):
12681318 # Overridden to make all the ConfigurationFile objects
12691319 # returned refer to the configs/ path instead of the
12701320 # properties/ path used by Configrations.
1271- return self . service . _abspath ( PATH_CONF % state ['title' ])
1321+ return PATH_CONF % state ['title' ]
12721322
12731323
12741324class Stanza (Entity ):
@@ -1353,6 +1403,20 @@ def clean(self, timeout=60):
13531403 raise OperationError , "Operation timed out."
13541404 return self
13551405
1406+ def disable (self ):
1407+ """Disables this index."""
1408+ # Starting in Ace, we have to do this with specific sharing,
1409+ # unlike most other entities.
1410+ self .post ("disable" , sharing = "system" )
1411+ return self
1412+
1413+ def enable (self ):
1414+ """Enables this index."""
1415+ # Starting in Ace, we have to reenable this with a specific
1416+ # sharing unlike most other entities.
1417+ self .post ("enable" , sharing = "system" )
1418+ return self
1419+
13561420 def roll_hot_buckets (self ):
13571421 """Performs rolling hot buckets for this index."""
13581422 self .post ("roll-hot-buckets" )
@@ -1451,6 +1515,15 @@ def __getitem__(self, key):
14511515 else :
14521516 raise KeyError (key )
14531517
1518+ def __contains__ (self , key ):
1519+ try :
1520+ self .__getitem__ (key )
1521+ return True
1522+ except KeyError :
1523+ return False
1524+ except AmbiguousReferenceException :
1525+ return True
1526+
14541527
14551528 def create (self , kind , name , ** kwargs ):
14561529 """Creates an input of a specific kind in this collection, with any
@@ -1549,7 +1622,8 @@ def oneshot(self, **kwargs):
15491622class Job (Entity ):
15501623 """This class represents a search job."""
15511624 def __init__ (self , service , path , ** kwargs ):
1552- Entity .__init__ (self , service , path , ** kwargs )
1625+ Entity .__init__ (self , service , path , skip_refresh = True , ** kwargs )
1626+ self ._isReady = False
15531627
15541628 # The Job entry record is returned at the root of the response
15551629 def _load_atom_entry (self , response ):
@@ -1581,6 +1655,28 @@ def finalize(self):
15811655 self .post ("control" , action = "finalize" )
15821656 return self
15831657
1658+ def isDone (self ):
1659+ """Has this job finished running on the server yet?
1660+
1661+ :returns: boolean
1662+ """
1663+ if (not self .isReady ()):
1664+ return False
1665+ return self ['isDone' ] == '1'
1666+
1667+ def isReady (self ):
1668+ """Is this job queryable on the server yet?
1669+
1670+ :returns: boolean
1671+ """
1672+ try :
1673+ self .refresh ()
1674+ self ._isReady = True
1675+ return self ._isReady
1676+ except JobNotReadyException :
1677+ self ._isReady = False
1678+ return False
1679+
15841680 @property
15851681 def name (self ):
15861682 """Returns the name of the search job."""
@@ -1591,20 +1687,35 @@ def pause(self):
15911687 self .post ("control" , action = "pause" )
15921688 return self
15931689
1594- def read (self ):
1595- """Returns the job's current state record, corresponding to the
1596- current state of the server-side resource."""
1597- # If the search job is newly created, it is possible that we will
1598- # get 204s (No Content) until the job is ready to respond.
1599- count = 0
1600- while count < 10 :
1690+ def refresh (self , state = None ):
1691+ """Refresh the state of this entity.
1692+
1693+ If *state* is provided, load it as the new state for this
1694+ entity. Otherwise, make a roundtrip to the server (by calling
1695+ the :meth:`read` method of self) to fetch an updated state,
1696+ plus at most two additional round trips if autologin is
1697+ enabled.
1698+
1699+ **Example**::
1700+
1701+ import splunklib.client as client
1702+ s = client.connect(...)
1703+ search = s.jobs.create('search index=_internal | head 1')
1704+ search.refresh()
1705+ """
1706+ if state is not None :
1707+ self ._state = state
1708+ else :
16011709 response = self .get ()
1602- if response .status == 204 :
1603- sleep (1 )
1604- count += 1
1605- continue
1606- return self ._load_state (response )
1607- raise OperationError , "Operation timed out."
1710+ if response .status == 204 :
1711+ self ._isReady = False
1712+ raise JobNotReadyException ()
1713+ else :
1714+ self ._isReady = True
1715+ raw_state = self ._load_state (response )
1716+ raw_state ['links' ] = dict ([(k , urllib .unquote (v )) for k ,v in raw_state ['links' ].iteritems ()])
1717+ self ._state = raw_state
1718+ return self
16081719
16091720 def results (self , timeout = None , wait_time = 1 , ** query_params ):
16101721 """Fetch search results as an InputStream IO handle.
@@ -1802,7 +1913,7 @@ def export(self, query, **params):
18021913 to two for create followed by preview), plus at most two more
18031914 if autologin is turned on.
18041915
1805- :raises SyntaxError : on invalid queries.
1916+ :raises ValueError : on invalid queries.
18061917
18071918 :param query: Splunk search language query to run
18081919 :type query: ``str``
@@ -1814,8 +1925,8 @@ def export(self, query, **params):
18141925 try :
18151926 return self .post (path_segment = "export" , search = query , ** params ).body
18161927 except HTTPError as he :
1817- if he .status == 400 and 'Search operation' in str ( he ) :
1818- raise SyntaxError (str (he ))
1928+ if he .status == 400 :
1929+ raise ValueError (str (he ))
18191930 else :
18201931 raise
18211932
@@ -1841,7 +1952,7 @@ def oneshot(self, query, **params):
18411952 to two for create followed by results), plus at most two more
18421953 if autologin is turned on.
18431954
1844- :raises SyntaxError : on invalid queries.
1955+ :raises ValueError : on invalid queries.
18451956
18461957 :param query: Splunk search language query to run
18471958 :type query: ``str``
@@ -1853,8 +1964,8 @@ def oneshot(self, query, **params):
18531964 try :
18541965 return self .post (search = query , exec_mode = "oneshot" , ** params ).body
18551966 except HTTPError as he :
1856- if he .status == 400 and 'Search operation' in str ( he ) :
1857- raise SyntaxError (str (he ))
1967+ if he .status == 400 :
1968+ raise ValueError (str (he ))
18581969 else :
18591970 raise
18601971
0 commit comments