diff --git a/LICENSE b/LICENSE index dbd51d0..4dcfdfa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright © 2010-2015, Rally Software Development Corp. Boulder, Colorado USA +Copyright © 2016, CA Technologies; 2010-2015, Rally Software Development Corp. Boulder, Colorado USA All rights reserved. License for pyral (a Python toolkit for the Rally REST API) diff --git a/README.rst b/README.rst index f2c7e26..73b040e 100644 --- a/README.rst +++ b/README.rst @@ -8,6 +8,11 @@ and productive Python language. The ``pyral`` package provides a smooth and easy to use veneer on top of the Rally REST Web Services API using JSON. +As of July 2015, the Rally Software Development company was acquired by CA Technologies. +The Rally product itself has been rebranded as 'Agile Central'. Over time, the documentation +will transition from using the term 'Rally' to using 'Agile Central'. + + .. contents:: Getting started @@ -34,7 +39,7 @@ Obtain the requests_ package and install it according to that package's directio As of requests-2.0.0, there is support for HTTPS over HTTP proxy via the CONNECT request. Use of requests-2.x or better is recommended for use with pyral. The requests_ package can be found via the Python Package Index site (http://pypi/python.org/index). -The most recent release of pyral (1.1.1) has been tested using requests 2.3.0. +The most recent release of pyral (1.2.0) has been tested using requests 2.8.1. Unpack the ``pyral`` distribution file (zip or tar.gz) and then install the pyral_ package. @@ -56,11 +61,11 @@ relevant packages. :: $ python - Python 2.7.5 [other Python interpreter info elided ...] + Python 2.7.11 [other Python interpreter info elided ...] >> import requests >> import pyral >> pyral.__version__ - (1, 1, 1) + (1, 2, 0) @@ -87,7 +92,7 @@ Full Documentation The complete documentation for the Python toolkit for Rally REST API is in the doc/build/html subdirectory in the repository. The rendered version of this is also available at -http://readthedocs.org/docs/pyral. +http://pyral.readthedocs.io/en/latest/ Sample code @@ -254,42 +259,63 @@ The item names in config files **are** case sensitive. Prerequisites ------------- - * Python 2.6 or 2.7 (2.7 is preferred) + * Python 2.6 or 2.7 (2.7 is preferred) OR + * Python 3.5 (this package not tested with earlier versions of Python 3.x) * The requests_ package, 2.0.0 or better (2.0.0 finally includes support for https proxy), - requests 2.3.0 is recommended. + requests 2.8.1 is recommended. .. _requests: http://github.com/kennethreitz/requests Versions -------- + 1.2.0 + Support for Python 3.5.x + Begin deprecation sequence for pinging the Rally server before the connection attempt, + initially with this version, allow option on instantiation to bypass ping. + Added ability to rankAbove, rankBelow, rankToTop, rankToBottom for an Artifact. + Fixed defect where user has no default workspace or project. + + addAttachment now correctly handles binary file, attachment size limit increased to 50MB to match Agile Central limit. + Exception generated when running getAllUsers when credentials are for non Subscription/Workspace Administrator has been fixed. + Added ability to work with a single Workspace, which has beneficial performance effect for Subscriptions with a large number of Workspaces. + Modified internal attribute handling to limit calls to get attribute's allowed values to qualifying attribute types. + Added examples/updtag.py script. + + 1.1.1 - - Modified entity.py to allow it to pass back PortfolioItem sub type instances. - - Modified rallyresp.py defect referencing non-existing req_type instance var by changing reference to request_type. - - Modified restapi.py to use user, dropped auth_user. - - Modified restapi.py to be more defensive when user has no associated UserProfile. - - Modified context.py to account for use of Cygwin in Pinger code. - - Modified restapi.py to handle encoding of attachment content to match Rally expectations. - - Modified restapi.py/entity.py to handle querying of SchedulableArtifact instances. - - Modified restapi.py to handle querying and hydrating of PortfolioItem instances more completely. - - Modified restapi.py/entity.py to provide rudimentary support for querying of RecycleBin entries. - - Modified restapi.py and added search_utils.py to provide a search method for pyral Rally instances. - - Modified rallyresp.py to better handle some boundary conditions when response body item counts differ from what is stated in the TotalResultCount. - - Modified context.py to account for scenario where user's default workspace has no projects. - - Modified restapi.py/getProject to return correct project. + Modified entity.py to allow it to pass back PortfolioItem sub type instances. + Modified rallyresp.py defect referencing non-existing req_type instance var by changing + reference to request_type. + Modified restapi.py to use user, dropped auth_user. + Modified restapi.py to be more defensive when user has no associated UserProfile. + Modified context.py to account for use of Cygwin in Pinger code. + Modified restapi.py to handle encoding of attachment content to match Rally expectations. + Modified restapi.py/entity.py to handle querying of SchedulableArtifact instances. + Modified restapi.py to handle querying and hydrating of PortfolioItem instances more completely. + Modified restapi.py/entity.py to provide rudimentary support for querying of RecycleBin entries. + Modified restapi.py and added search_utils.py to provide a search method for pyral Rally instances. + Modified rallyresp.py to better handle some boundary conditions when response body item counts + differ from what is stated in the TotalResultCount. + Modified context.py to account for scenario where user's default workspace has no projects. + Modified restapi.py/getProject to return correct project. 1.1.0 - - Introduction of support to use Rally API Key and rallyWorkset (supercedes rallySettings). - - Two relatively minor defects fixed dealing with internalizing environment vars for initialization and in retrieving Rally entity attribute allowed values. + Introduction of support to use Rally API Key and rallyWorkset (supercedes rallySettings). + Two relatively minor defects fixed dealing with internalizing environment + vars for initialization and in retrieving Rally entity attribute allowed values. 1.0.1 - - Patch to address defect with Rally WSAPI v2.0 projects collection endpoint providing conflicting information. + Patch to address defect with Rally WSAPI v2.0 projects collection endpoint + providing conflicting information. 1.0.0 - - Default WSAPI version in config is v2.0. This version is not compatible with Rally WSAPI version 1.x. - - Adjusted the RallyUrlBuilder (via RallyQueryFormatter) to be more resilient with respect to many more "special" characters (non-alphanumeric). - - Retrieving the meta data uses the v2.0 schema endpoint. - - No longer support a version keyword argument when obtaining a Rally instance. + Default WSAPI version in config is v2.0. This version is not compatible + with Rally WSAPI version 1.x. + Adjusted the RallyUrlBuilder (via RallyQueryFormatter) to be more resilient + with respect to many more "special" characters (non-alphanumeric). + Retrieving the meta data uses the v2.0 schema endpoint. + No longer support a version keyword argument when obtaining a Rally instance. 0.9.4 Adjusted Rally __init__ to accommodate using requests 0.x, 1.x, 2.x versions. @@ -355,15 +381,14 @@ Versions TODO ---- -* Rework the distribution to be pip installable +* Investigate permanent location for web-access to rendered documentation * Dynamically construct the Rally schema hierarchy economically. -* Python 3.3+ support License ------- -BSD3-style license. Copyright (c) 2010-2015 Rally Software Development. +BSD3-style license. Copyright (c) 2015-2016 CA Technologies, 2010-2015 Rally Software Development. See the LICENSE file provided with the source distribution for full details. diff --git a/README.short b/README.short index 0eccb64..7f1d773 100644 --- a/README.short +++ b/README.short @@ -20,7 +20,7 @@ Requirements The pyral package requires the use of Kenneth Reitz's requests package using version 2.0.0 or better. As of requests version 2.0.0, there is support for HTTPS over HTTP proxy via the CONNECT request. The requests package can be found via the Python Package Index site (http://pypi/python.org/index). -The most recent release of pyral (1.1.1) has been tested with requests 2.3.0. +The most recent release of pyral (1.2.0) has been tested with requests 2.8.1. Installation @@ -48,5 +48,5 @@ Documentation Obtain the zip file containing the HTML and CSS files comprising the document set for pyral. Unzip in a suitable location and point your web browser to the doc/build/html/index.html file -The rendered documentation is also available at http://readthedocs.org/docs/pyral . + The rendered documentation is also available at http://pyral.readthedocs.io/en/latest/ diff --git a/VERSIONS b/VERSIONS index e6c0f29..8ca0f5b 100644 --- a/VERSIONS +++ b/VERSIONS @@ -72,3 +72,18 @@ Added ability to query RecycleBin entries. Added basic search method to Rally instance. +1.1.2 - February 2016 + internal release only + minor style mods for Pinger + fixed handling of proxy value under some edge conditions + exploratory support for Python 3.5 + +1.2.0 - Oct 2016 + support for Python 3.5 + allow instantiation option to bypass use of Pinger + changed header to reflect change of company name to CA Technologies + added ranking convenience methods + modified handling of attribute allowedValues for better create/update performance + allow instantiation option to work with only one workspace for better performance for Subscriptions with large number of Workspaces + fix addAttachment handling to allow binary files and increase size limit to 50MB. + diff --git a/build_dist.py b/build_dist.py index d10522b..92ec2c4 100755 --- a/build_dist.py +++ b/build_dist.py @@ -11,7 +11,7 @@ import zipfile PACKAGE_NAME = "pyral" -VERSION = "1.1.1" +VERSION = "1.2.0" AUX_FILES = ['MANIFEST.in', 'LICENSE', @@ -36,7 +36,8 @@ 'get_attachments.py', 'get_schedulable_artifacts.py', 'add_tcrs.py', - 'defrevs.py' + 'defrevs.py', + 'updtag.py' ] DOC_FILES = ['doc/Makefile', 'doc/source/conf.py', @@ -67,6 +68,8 @@ 'test/test_search.py', 'test/test_wksprj_setting.py', 'test/test_attachments.py', + 'test/test_workspaces.py' + 'test/test_ranking.py' ] ################################################################################ @@ -80,10 +83,13 @@ def main(args): zf = zipfile.ZipFile(zipped, 'r') for info in zf.infolist(): - #print info.filename, info.date_time, info.file_size, info.compress_size - reduction_fraction = float(info.compress_size) / float(info.file_size) + #print(info.filename, info.date_time, info.file_size, info.compress_size) + if info.file_size: + reduction_fraction = float(info.compress_size) / float(info.file_size) + else: + reduction_fraction = 0.0 reduction_pct = int(reduction_fraction * 100) - print "%-52.52s %6d (%2d%%)" % (info.filename, info.compress_size, reduction_pct) + print("%-52.52s %6d (%2d%%)" % (info.filename, info.compress_size, reduction_pct)) ################################################################################ diff --git a/dists/pyral-1.2.0.tar.gz b/dists/pyral-1.2.0.tar.gz new file mode 100644 index 0000000..2f18dd3 Binary files /dev/null and b/dists/pyral-1.2.0.tar.gz differ diff --git a/dists/pyral-1.2.0.zip b/dists/pyral-1.2.0.zip new file mode 100644 index 0000000..bf413ad Binary files /dev/null and b/dists/pyral-1.2.0.zip differ diff --git a/doc/build/doctrees/environment.pickle b/doc/build/doctrees/environment.pickle index a897753..a7df82e 100644 Binary files a/doc/build/doctrees/environment.pickle and b/doc/build/doctrees/environment.pickle differ diff --git a/doc/build/doctrees/index.doctree b/doc/build/doctrees/index.doctree index 4c8a9ca..89103e9 100644 Binary files a/doc/build/doctrees/index.doctree and b/doc/build/doctrees/index.doctree differ diff --git a/doc/build/doctrees/interface.doctree b/doc/build/doctrees/interface.doctree index c44cef3..b4a5777 100644 Binary files a/doc/build/doctrees/interface.doctree and b/doc/build/doctrees/interface.doctree differ diff --git a/doc/build/doctrees/overview.doctree b/doc/build/doctrees/overview.doctree index 1ee32dd..bb28551 100644 Binary files a/doc/build/doctrees/overview.doctree and b/doc/build/doctrees/overview.doctree differ diff --git a/doc/build/html/.buildinfo b/doc/build/html/.buildinfo index 7a8ceda..bc8b786 100644 --- a/doc/build/html/.buildinfo +++ b/doc/build/html/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: ab43c34d713cfed30f66981a3090babd +config: bc481c91bc67b7eba85aa1ce56db2b8a tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/doc/build/html/_sources/interface.txt b/doc/build/html/_sources/interface.txt index 93ddd68..4a492d7 100644 --- a/doc/build/html/_sources/interface.txt +++ b/doc/build/html/_sources/interface.txt @@ -18,26 +18,31 @@ of the **RallyRESTResponse** class. Instances of this class allow easy dot ('.' access to attributes of the representation of the Rally entity, whether the attribute is a simple value or a reference to another Rally entity. -rallySettings -============= +rallyWorkset +============ + +New in 1.1.0. This function takes into account your environment and arguments provided to this function to arrive at and return information necessary to establish a useful *connection* to the -Rally server. +Rally server. This convenience function differs from **rallySettings** by also including +the Rally API Key in the set of values considered and returned. The process consists of a priority chain where some reasonable default information is established first and then overridden with subsequent steps in the chain (if they exist). -After following the priority chain, values for server, user, password, workspace, project -are returned to the caller. +After following the priority chain, values for server, user, password, apikey, workspace, +project are returned to the caller. The priority chain consists of these steps: - * establish baseline values from values defined in the module containing the rallySettings + * establish baseline values from values defined in the module containing the rallyWorkset * override with any environment variables present from this list: - RALLY_SERVER - RALLY_USER - RALLY_PASSWORD + - APIKEY - RALLY_WORKSPACE - RALLY_PROJECT + - RALLY_PING * if present, use information from a rally-.cfg file in the current directory, where matches the Rally WSAPI version defined in the pyral.config module. Currently, that version is defined as v2.0. @@ -57,8 +62,10 @@ The specific syntax available for these levels is detailed below. - SERVER = - USER = - PASSWORD = + - APIKEY = - WORKSPACE = - PROJECT = + - RALLY_PING = True | False **Command line options** @@ -76,9 +83,13 @@ The specific syntax available for these levels is detailed below. --rallyPassword= + --apikey= + --workspace= --project= + + --ping=True|False|true|false|yes|no|1|0 This mechanism provides the ability to centrally locate a configuration file that can be used by many members of a team where server, workspace, project are common to all members @@ -86,6 +97,15 @@ and each individual can have their own appropriately secured config file with th Using this mechanism can save tedious and error-prone entry of target information and credentials on the command line or having credential information in clear text in unsecured files. +The use of a Rally API Key value for identification/authentication is new in pyral 1.1.x. +If used, you do not need to provide a username / password combination. +In order to use this, you must first obtain a valid API Key value from the Rally Application +Manager (API Keys) that you can access from https://rally1.rallydev.com/login. +Once obtained, you should treat the key with the same level of protection as you would +any user/password information; once presented to Rally via the Rally Web Services API, +a connection has all the rights associated with the user whose key was presented. +Consult the Rally help documentation for further information. + Example use:: % export RALLY_SERVER="rally1.rallydev.com" @@ -104,45 +124,44 @@ Example use:: % cat basic.py import sys - from rally import rallySettings + from rally import rallyWorkset options = [opt for opt in sys.argv[1:] if opt.startswith('--')] - server, user, password, workspace, project = rallySettings(options) - print " ".join(['|%|' % opt for opt in [server, user, password, workspace, project]] + server, user, password, apikey, workspace, project = rallyWorkset(options) + print " ".join(['|%|' % opt for opt in [server, user, password, apikey, workspace, project]] - % python basic.py --config=current --rallyPassword='*****' --rallyProject="Livestock Mgmt" + % python basic.py --config=current --rallyProject="Livestock Mgmt" --ping=no - |rally1.rallydev.com| |wiley@acme.com| |*****| |General Products Umbrella| |Livestock Mgmt| + |rally1.rallydev.com| |wiley@acme.com| |*****| |*****| |General Products Umbrella| |Livestock Mgmt| Note that for convenience purposes a configuration file name may be fully specified or you may elect to not specify the '.cfg' suffix. -Returns a tuple of (server, username, password, workspace, project) +Returns a tuple of (server, username, password, apikey, workspace, project) -rallyWorkset -============ +rallySettings +============= -New in 1.1.0. +This is deprecated as of v1.2.0. The preferred function is **rallyWorkset** which will have +ongoing support. The **rallySettings** function will be removed in v2.0.0. This function takes into account your environment and arguments provided to this function to arrive at and return information necessary to establish a useful *connection* to the -Rally server. This convenience function differs from **rallySettings** by also including -the Rally API Key in the set of values considered and returned. +Rally server. The process consists of a priority chain where some reasonable default information is established first and then overridden with subsequent steps in the chain (if they exist). -After following the priority chain, values for server, user, password, apikey, workspace, -project are returned to the caller. +After following the priority chain, values for server, user, password, workspace, project +are returned to the caller. The priority chain consists of these steps: - * establish baseline values from values defined in the module containing the rallyWorkset + * establish baseline values from values defined in the module containing the rallySettings * override with any environment variables present from this list: - RALLY_SERVER - RALLY_USER - RALLY_PASSWORD - - APIKEY - RALLY_WORKSPACE - RALLY_PROJECT * if present, use information from a rally-.cfg file in the current directory, @@ -264,6 +283,29 @@ Rally and the default project for the user is not in the workspace specified. Under those conditions, the project is changed to the first project (alphabetic ordering) in the list of projects for the specified workspace. + * server_ping (True or False, default in v1.2.0 is True) + Specifies whether a ping attempt will be made to confirm network connectivity + to the Rally server prior to making a Rally WSAPI REST request. + Organizations may have disabled the ability to make ICMP requests so the ping + attempt may fail even though there is network connectivity to the Rally server. + For this reason, the use of the ping=True option is discouraged going forward. + The next minor point release of pyral (v1.3.0) will have the default value + for this option inverted to be False. + The the ping operation itself will be dropped in the next major release (2.0.0). + * isolated_workspace (True or False, default in v1.2.0 is False) + Specifies that the Rally instance will only be used for interacting with + a single workspace (either the user's default workspace or the named workspace). + Using isolated_workspace=True provides performance benefits for a subscription + with many workspaces, but it also means you cannot change the workspace you + are working within a single instance of a Rally class, nor can you provide + a workspace keyword argument to a get, create, update or delete methods that + differs from the workspace identified at instantiation time. + For subscriptions with a small to moderate number of workspaces (up to a few dozen), + the performance savings will be relatively minor when using isolated_workspace=True + vs. isolated_workspace=False. However, for subscriptions with a large number of + workspaces, using isolated_workspace=False results in a request to AgileCentral + for each workspace, which can result in a noticeable lag before the instantiation + statement returns a ready-for-use Rally instance. If you use an apikey value, any user name and password you provide is not considered, the connection attempt will only use the apikey. @@ -276,7 +318,7 @@ Rally **pyral**, you must have your account added to the whitelist in Rally so that you can use either BasicAuth (username and password) or the API Key to authenticate to Rally. -.. py:class:: Rally (server, user=None, password=None, apikey=None, workspace=None, project=None, warn=True) +.. py:class:: Rally (server, user=None, password=None, apikey=None, workspace=None, project=None, warn=True, server_ping=True) Examples:: @@ -290,6 +332,8 @@ Examples:: rally = Rally(server, apikey="_some-more-numbers", workspace='RockLobster', project='Fence Posts') + rally = Rally('rally1.rallydev.com', 'chester@corral.com', 'bAbYF@cerZ', server_ping=False) + Core REST methods and CRUD aliases @@ -513,6 +557,9 @@ pyral.Rally instance convenience methods This method offers a convenient one-stop means of obtaining usable information about all users in the named workspace. If no workspace is specified, then the current context's workspace is used. + NOTE: Unless you are using credentials associated with a SubscriptionAdministrator + or WorkspaceAdministrator, you will not be able to access a user's UserProfile + other than yourself. Return a list of User instances (fully hydrated for scalar attributes) whose ref and collection attributes will be lazy eval'ed upon access. @@ -525,6 +572,16 @@ pyral.Rally instance convenience methods PortfolioItem subclass. Intended usage is to use the return *.ref* attribute. For example, within an info dict, "PortfolioItemType" : rally.typedef('Feature').ref . +.. method:: getCollection(collection_url) + + Given a collection_url of the form: + + http(s)://(:)/slm/webservice/v2.0//OID/ + + issue a request for the url and return back a list of hydrated instances + for each item in the collection. + + .. method:: getState(entityName, stateName) As of Rally WSAPI 1.37 (Sep 2012), the State attribute is no longer a String value for @@ -587,6 +644,23 @@ pyral.Rally instance convenience methods Each Attachment record will look like a Rally WSAPI Attachment with the additional Content attribute that will contain the decoded AttachmentContent. +.. method:: rankAbove(reference_artifact, target_artifact) + + Rank the target_artifact above the reference_artifact. + +.. method:: rankBelow(reference_artifact, target_artifact) + + Rank the target_artifact below the reference_artifact. + +.. method:: rankTop(target_artifact) + + Rank the target_artifact at the top of the list of ranked Artifacts + that the target_artifact exists in. + +.. method:: rankBottom(target_artifact) + + Rank the target_artifact at the bottom of the list of ranked Artifacts + that the target_artifact exists in. RallyRESTResponse @@ -649,7 +723,7 @@ Item Attributes the standard dot (.) notation. The names are identical to those documented in the `Rally WS API`_. -.. _Rally WS API: https://rally.rallydev.com/slm/doc/webservice +.. _Rally WS API: https://rally1.rallydev.com/slm/doc/webservice Generally, every concrete instance in the Rally system will have a Name attribute. You can use the **attributes()** method on an instance to obtain the names of all of the @@ -697,7 +771,7 @@ Example:: oid : 12345678 ref : hierarchicalrequirement/12345678 ObjectID : 12345678 - _ref : https://rallydev.rallydev.com/slm/webservice/1.30/hierarchicalrequirement/12345678.js + _ref : https://rallydev.rallydev.com/slm/webservice/v2.0/hierarchicalrequirement/412345678 _CreatedAt : today at 3:14 am _hydrated : True Name : Filbert nuts should be added to all energy bars @@ -715,16 +789,16 @@ Example:: Capitalizable : None Changesets : [] Children : [] - CreationDate : 2012-07-12T09:14:35.852Z + CreationDate : 2016-07-12T09:14:35.852Z DefectStatus : NONE Defects : [] Description : As a health conscious PO, I want better nutritional content in all bars Discussion : [] IdeaURL : IdeaVotes : None - InProgressDate : 2012-07-12T09:14:36.098Z + InProgressDate : 2016-07-12T09:14:36.098Z Iteration : Iteration.ref (OID 1242381 Name Iteration 5 (Summer)) KanbanState : Accepted - LastUpdateDate : 2012-07-12T09:14:36.237Z + LastUpdateDate : 2016-07-12T09:14:36.237Z ... diff --git a/doc/build/html/_sources/overview.txt b/doc/build/html/_sources/overview.txt index 21d6790..116c043 100644 --- a/doc/build/html/_sources/overview.txt +++ b/doc/build/html/_sources/overview.txt @@ -68,7 +68,7 @@ In the Rally vernacular, a logical entity is called a *type*. Some examples of *types* that are usually what a user of **pyral** will be interested in called *artifacts*. An *artifact* is either a UserStory, Defect, Task, DefectSuite, TestCase or TestSet. The Python toolkit for the Rally REST API (**pyral**) is primarily oriented towards operations with artifacts. -But, it is not limited to those as it is very possible view/operate on other Rally +But, it is not limited to those as it is very possible to view/operate on other Rally entities such as Workspace, Project, UserProfile, Release, Iteration, TestCaseResult, TestFolder, Tag and others. diff --git a/doc/build/html/_static/css/badge_only.css b/doc/build/html/_static/css/badge_only.css new file mode 100644 index 0000000..7e17fb1 --- /dev/null +++ b/doc/build/html/_static/css/badge_only.css @@ -0,0 +1,2 @@ +.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:0.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}} +/*# sourceMappingURL=badge_only.css.map */ diff --git a/doc/build/html/_static/css/theme.css b/doc/build/html/_static/css/theme.css new file mode 100644 index 0000000..a2d7c0f --- /dev/null +++ b/doc/build/html/_static/css/theme.css @@ -0,0 +1,5 @@ +*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,.rst-content code,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:0.2em 0;background:#ccc;color:#000;padding:0.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none !important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{html,body,section{background:none !important}*{box-shadow:none !important;text-shadow:none !important;filter:none !important;-ms-filter:none !important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,.rst-content .toctree-wrapper p.caption,h3{orphans:3;widows:3}h2,.rst-content .toctree-wrapper p.caption,h3{page-break-after:avoid}}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.2.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff?v=4.2.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.2.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:0.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:0.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.wy-menu-vertical li span.pull-left.toctree-expand,.wy-menu-vertical li.on a span.pull-left.toctree-expand,.wy-menu-vertical li.current>a span.pull-left.toctree-expand,.rst-content .pull-left.admonition-title,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content dl dt .pull-left.headerlink,.rst-content p.caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.rst-content code.download span.pull-left:first-child,.pull-left.icon{margin-right:.3em}.fa.pull-right,.wy-menu-vertical li span.pull-right.toctree-expand,.wy-menu-vertical li.on a span.pull-right.toctree-expand,.wy-menu-vertical li.current>a span.pull-right.toctree-expand,.rst-content .pull-right.admonition-title,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content dl dt .pull-right.headerlink,.rst-content p.caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.rst-content code.download span.pull-right:first-child,.pull-right.icon{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-remove:before,.fa-close:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-warning:before,.fa-exclamation-triangle:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-gears:before,.fa-cogs:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.wy-dropdown .caret:before,.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li span.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:""}.fa-meanpath:before{content:""}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context{font-family:inherit}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before{font-family:"FontAwesome";display:inline-block;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa,a .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink,a .rst-content p.caption .headerlink,.rst-content p.caption a .headerlink,a .rst-content tt.download span:first-child,.rst-content tt.download a span:first-child,a .rst-content code.download span:first-child,.rst-content code.download a span:first-child,a .icon{display:inline-block;text-decoration:inherit}.btn .fa,.btn .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .btn span.toctree-expand,.btn .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .btn span.toctree-expand,.btn .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .btn span.toctree-expand,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.btn .rst-content p.caption .headerlink,.rst-content p.caption .btn .headerlink,.btn .rst-content tt.download span:first-child,.rst-content tt.download .btn span:first-child,.btn .rst-content code.download span:first-child,.rst-content code.download .btn span:first-child,.btn .icon,.nav .fa,.nav .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .nav span.toctree-expand,.nav .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .nav span.toctree-expand,.nav .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .nav span.toctree-expand,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink,.nav .rst-content p.caption .headerlink,.rst-content p.caption .nav .headerlink,.nav .rst-content tt.download span:first-child,.rst-content tt.download .nav span:first-child,.nav .rst-content code.download span:first-child,.rst-content code.download .nav span:first-child,.nav .icon{display:inline}.btn .fa.fa-large,.btn .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .btn span.fa-large.toctree-expand,.btn .rst-content .fa-large.admonition-title,.rst-content .btn .fa-large.admonition-title,.btn .rst-content h1 .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.btn .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .btn .fa-large.headerlink,.btn .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .btn .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .btn span.fa-large:first-child,.btn .rst-content code.download span.fa-large:first-child,.rst-content code.download .btn span.fa-large:first-child,.btn .fa-large.icon,.nav .fa.fa-large,.nav .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .nav span.fa-large.toctree-expand,.nav .rst-content .fa-large.admonition-title,.rst-content .nav .fa-large.admonition-title,.nav .rst-content h1 .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.nav .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.nav .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .nav .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.nav .rst-content code.download span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.nav .fa-large.icon{line-height:0.9em}.btn .fa.fa-spin,.btn .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .btn span.fa-spin.toctree-expand,.btn .rst-content .fa-spin.admonition-title,.rst-content .btn .fa-spin.admonition-title,.btn .rst-content h1 .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.btn .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .btn .fa-spin.headerlink,.btn .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .btn .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .btn span.fa-spin:first-child,.btn .rst-content code.download span.fa-spin:first-child,.rst-content code.download .btn span.fa-spin:first-child,.btn .fa-spin.icon,.nav .fa.fa-spin,.nav .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .nav span.fa-spin.toctree-expand,.nav .rst-content .fa-spin.admonition-title,.rst-content .nav .fa-spin.admonition-title,.nav .rst-content h1 .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.nav .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.nav .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .nav .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.nav .rst-content code.download span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.nav .fa-spin.icon{display:inline-block}.btn.fa:before,.wy-menu-vertical li span.btn.toctree-expand:before,.rst-content .btn.admonition-title:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content dl dt .btn.headerlink:before,.rst-content p.caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.rst-content code.download span.btn:first-child:before,.btn.icon:before{opacity:0.5;-webkit-transition:opacity 0.05s ease-in;-moz-transition:opacity 0.05s ease-in;transition:opacity 0.05s ease-in}.btn.fa:hover:before,.wy-menu-vertical li span.btn.toctree-expand:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content p.caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.rst-content code.download span.btn:first-child:hover:before,.btn.icon:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li .btn-mini span.toctree-expand:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before,.btn-mini .rst-content p.caption .headerlink:before,.rst-content p.caption .btn-mini .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.rst-content tt.download .btn-mini span:first-child:before,.btn-mini .rst-content code.download span:first-child:before,.rst-content code.download .btn-mini span:first-child:before,.btn-mini .icon:before{font-size:14px;vertical-align:-15%}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:#6ab0de;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.admonition-todo{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso,.rst-content .admonition-todo{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .admonition-todo .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .admonition-todo .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.rst-content .seealso,.rst-content .wy-alert-info.admonition-todo{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title,.rst-content .wy-alert-info.admonition-todo .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.admonition-todo{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.admonition-todo .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.admonition-todo{background:#f3f6f6}.wy-alert.wy-alert-neutral .wy-alert-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .admonition-title{color:#404040;background:#e1e4e5}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.admonition-todo a{color:#2980B9}.wy-alert p:last-child,.rst-content .note p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.rst-content .seealso p:last-child,.rst-content .admonition-todo p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0px;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all 0.3s ease-in;-moz-transition:all 0.3s ease-in;transition:all 0.3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27AE60}.wy-tray-container li.wy-tray-item-info{background:#2980B9}.wy-tray-container li.wy-tray-item-warning{background:#E67E22}.wy-tray-container li.wy-tray-item-danger{background:#E74C3C}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width: 768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);background-color:#27AE60;text-decoration:none;font-weight:normal;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset;outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all 0.1s linear;-moz-transition:all 0.1s linear;transition:all 0.1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:0px -1px 0px 0px rgba(0,0,0,0.05) inset,0px 2px 0px 0px rgba(0,0,0,0.1) inset;padding:8px 12px 6px 12px}.btn:visited{color:#fff}.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980B9 !important}.btn-info:hover{background-color:#2e8ece !important}.btn-neutral{background-color:#f3f6f6 !important;color:#404040 !important}.btn-neutral:hover{background-color:#e5ebeb !important;color:#404040}.btn-neutral:visited{color:#404040 !important}.btn-success{background-color:#27AE60 !important}.btn-success:hover{background-color:#295 !important}.btn-danger{background-color:#E74C3C !important}.btn-danger:hover{background-color:#ea6153 !important}.btn-warning{background-color:#E67E22 !important}.btn-warning:hover{background-color:#e98b39 !important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f !important}.btn-link{background-color:transparent !important;color:#2980B9;box-shadow:none;border-color:transparent !important}.btn-link:hover{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:active{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:visited{color:#9B59B6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:before,.wy-btn-group:after{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:solid 1px #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,0.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980B9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:solid 1px #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type="search"]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980B9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 0.3125em 0;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#E74C3C}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full input[type="text"],.wy-control-group .wy-form-full input[type="password"],.wy-control-group .wy-form-full input[type="email"],.wy-control-group .wy-form-full input[type="url"],.wy-control-group .wy-form-full input[type="date"],.wy-control-group .wy-form-full input[type="month"],.wy-control-group .wy-form-full input[type="time"],.wy-control-group .wy-form-full input[type="datetime"],.wy-control-group .wy-form-full input[type="datetime-local"],.wy-control-group .wy-form-full input[type="week"],.wy-control-group .wy-form-full input[type="number"],.wy-control-group .wy-form-full input[type="search"],.wy-control-group .wy-form-full input[type="tel"],.wy-control-group .wy-form-full input[type="color"],.wy-control-group .wy-form-halves input[type="text"],.wy-control-group .wy-form-halves input[type="password"],.wy-control-group .wy-form-halves input[type="email"],.wy-control-group .wy-form-halves input[type="url"],.wy-control-group .wy-form-halves input[type="date"],.wy-control-group .wy-form-halves input[type="month"],.wy-control-group .wy-form-halves input[type="time"],.wy-control-group .wy-form-halves input[type="datetime"],.wy-control-group .wy-form-halves input[type="datetime-local"],.wy-control-group .wy-form-halves input[type="week"],.wy-control-group .wy-form-halves input[type="number"],.wy-control-group .wy-form-halves input[type="search"],.wy-control-group .wy-form-halves input[type="tel"],.wy-control-group .wy-form-halves input[type="color"],.wy-control-group .wy-form-thirds input[type="text"],.wy-control-group .wy-form-thirds input[type="password"],.wy-control-group .wy-form-thirds input[type="email"],.wy-control-group .wy-form-thirds input[type="url"],.wy-control-group .wy-form-thirds input[type="date"],.wy-control-group .wy-form-thirds input[type="month"],.wy-control-group .wy-form-thirds input[type="time"],.wy-control-group .wy-form-thirds input[type="datetime"],.wy-control-group .wy-form-thirds input[type="datetime-local"],.wy-control-group .wy-form-thirds input[type="week"],.wy-control-group .wy-form-thirds input[type="number"],.wy-control-group .wy-form-thirds input[type="search"],.wy-control-group .wy-form-thirds input[type="tel"],.wy-control-group .wy-form-thirds input[type="color"]{width:100%}.wy-control-group .wy-form-full{float:left;display:block;margin-right:2.35765%;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child{margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n+1){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child{margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control{margin:6px 0 0 0;font-size:90%}.wy-control-no-input{display:inline-block;margin:6px 0 0 0;font-size:90%}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:0.3em;color:#666;vertical-align:middle;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:0.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;*overflow:visible}input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}input[type="datetime-local"]{padding:0.34375em 0.625em}input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:0.3125em;*height:13px;*width:13px}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}input[type="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#333}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129FEA}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#E74C3C;border:1px solid #E74C3C}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#E74C3C}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]:focus:invalid:focus{outline-color:#E74C3C}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif}select,textarea{padding:0.5em 0.625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type="radio"][disabled],input[type="checkbox"][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:solid 1px #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{width:36px;height:12px;margin:12px 0;position:relative;border-radius:4px;background:#ccc;cursor:pointer;-webkit-transition:all 0.2s ease-in-out;-moz-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}.wy-switch:before{position:absolute;content:"";display:block;width:18px;height:18px;border-radius:4px;background:#999;left:-3px;top:-3px;-webkit-transition:all 0.2s ease-in-out;-moz-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}.wy-switch:after{content:"false";position:absolute;left:48px;display:block;font-size:12px;color:#ccc}.wy-switch.active{background:#1e8449}.wy-switch.active:before{left:24px;background:#27AE60}.wy-switch.active:after{content:"true"}.wy-switch.disabled,.wy-switch.active.disabled{cursor:not-allowed}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#E74C3C}.wy-control-group.wy-control-group-error input[type="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.wy-control-group.wy-control-group-error input[type="search"],.wy-control-group.wy-control-group-error input[type="tel"],.wy-control-group.wy-control-group-error input[type="color"]{border:solid 1px #E74C3C}.wy-control-group.wy-control-group-error textarea{border:solid 1px #E74C3C}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:0.5em 0.625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27AE60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#E74C3C}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#E67E22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980B9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width: 480px){.wy-form button[type="submit"]{margin:0.7em 0 0}.wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0.3em;display:block}.wy-form label{margin-bottom:0.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:0.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width: 768px){.tablet-hide{display:none}}@media screen and (max-width: 480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px}.wy-table td p:last-child,.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child{margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0 !important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980B9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9B59B6}html{height:100%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#E67E22 !important}a.wy-text-warning:hover{color:#eb9950 !important}.wy-text-info{color:#2980B9 !important}a.wy-text-info:hover{color:#409ad5 !important}.wy-text-success{color:#27AE60 !important}a.wy-text-success:hover{color:#36d278 !important}.wy-text-danger{color:#E74C3C !important}a.wy-text-danger:hover{color:#ed7669 !important}.wy-text-neutral{color:#404040 !important}a.wy-text-neutral:hover{color:#595959 !important}h1,h2,.rst-content .toctree-wrapper p.caption,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif}p{line-height:24px;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2,.rst-content .toctree-wrapper p.caption{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}code,.rst-content tt,.rst-content code{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #e1e4e5;font-size:75%;padding:0 5px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;color:#E74C3C;overflow-x:auto}code.code-large,.rst-content tt.code-large{font-size:90%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li p:last-child,.rst-content .section ul li p:last-child,.rst-content .toctree-wrapper ul li p:last-child,article ul li p:last-child{margin-bottom:0}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul,article ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,article ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,article ul li li li{list-style:square}.wy-plain-list-disc li ol li,.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,article ul li ol li{list-style:decimal}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li,article ol li{list-style:decimal;margin-left:24px}.wy-plain-list-decimal li p:last-child,.rst-content .section ol li p:last-child,.rst-content ol.arabic li p:last-child,article ol li p:last-child{margin-bottom:0}.wy-plain-list-decimal li ul,.rst-content .section ol li ul,.rst-content ol.arabic li ul,article ol li ul{margin-bottom:0}.wy-plain-list-decimal li ul li,.rst-content .section ol li ul li,.rst-content ol.arabic li ul li,article ol li ul li{list-style:disc}.codeblock-example{border:1px solid #e1e4e5;border-bottom:none;padding:24px;padding-top:48px;font-weight:500;background:#fff;position:relative}.codeblock-example:after{content:"Example";position:absolute;top:0px;left:0px;background:#9B59B6;color:#fff;padding:6px 12px}.codeblock-example.prettyprint-example-only{border:1px solid #e1e4e5;margin-bottom:24px}.codeblock,pre.literal-block,.rst-content .literal-block,.rst-content pre.literal-block,div[class^='highlight']{border:1px solid #e1e4e5;padding:0px;overflow-x:auto;background:#fff;margin:1px 0 24px 0}.codeblock div[class^='highlight'],pre.literal-block div[class^='highlight'],.rst-content .literal-block div[class^='highlight'],div[class^='highlight'] div[class^='highlight']{border:none;background:none;margin:0}div[class^='highlight'] td.code{width:100%}.linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 12px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;font-size:12px;line-height:1.5;color:#d9d9d9}div[class^='highlight'] pre{white-space:pre;margin:0;padding:12px 12px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;font-size:12px;line-height:1.5;display:block;overflow:auto;color:#404040}@media print{.codeblock,pre.literal-block,.rst-content .literal-block,.rst-content pre.literal-block,div[class^='highlight'],div[class^='highlight'] pre{white-space:pre-wrap}}.hll{background-color:#ffc;margin:0 -12px;padding:0 12px;display:block}.c{color:#998;font-style:italic}.err{color:#a61717;background-color:#e3d2d2}.k{font-weight:bold}.o{font-weight:bold}.cm{color:#998;font-style:italic}.cp{color:#999;font-weight:bold}.c1{color:#998;font-style:italic}.cs{color:#999;font-weight:bold;font-style:italic}.gd{color:#000;background-color:#fdd}.gd .x{color:#000;background-color:#faa}.ge{font-style:italic}.gr{color:#a00}.gh{color:#999}.gi{color:#000;background-color:#dfd}.gi .x{color:#000;background-color:#afa}.go{color:#888}.gp{color:#555}.gs{font-weight:bold}.gu{color:purple;font-weight:bold}.gt{color:#a00}.kc{font-weight:bold}.kd{font-weight:bold}.kn{font-weight:bold}.kp{font-weight:bold}.kr{font-weight:bold}.kt{color:#458;font-weight:bold}.m{color:#099}.s{color:#d14}.n{color:#333}.na{color:teal}.nb{color:#0086b3}.nc{color:#458;font-weight:bold}.no{color:teal}.ni{color:purple}.ne{color:#900;font-weight:bold}.nf{color:#900;font-weight:bold}.nn{color:#555}.nt{color:navy}.nv{color:teal}.ow{font-weight:bold}.w{color:#bbb}.mf{color:#099}.mh{color:#099}.mi{color:#099}.mo{color:#099}.sb{color:#d14}.sc{color:#d14}.sd{color:#d14}.s2{color:#d14}.se{color:#d14}.sh{color:#d14}.si{color:#d14}.sx{color:#d14}.sr{color:#009926}.s1{color:#d14}.ss{color:#990073}.bp{color:#999}.vc{color:teal}.vg{color:teal}.vi{color:teal}.il{color:#099}.gc{color:#999;background-color:#EAF2F5}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs li code,.wy-breadcrumbs li .rst-content tt,.rst-content .wy-breadcrumbs li tt{padding:5px;border:none;background:none}.wy-breadcrumbs li code.literal,.wy-breadcrumbs li .rst-content tt.literal,.rst-content .wy-breadcrumbs li tt.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{height:32px;display:inline-block;line-height:32px;padding:0 1.618em;margin-bottom:0;display:block;font-weight:bold;text-transform:uppercase;font-size:80%;color:#555;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:0.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li code,.wy-menu-vertical li .rst-content tt,.rst-content .wy-menu-vertical li tt{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li span.toctree-expand{display:block;float:left;margin-left:-1.2em;font-size:0.8em;line-height:1.6em;color:#4d4d4d}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:0.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.on a:hover span.toctree-expand,.wy-menu-vertical li.current>a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand{display:block;font-size:0.8em;line-height:1.6em;color:#333}.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul{display:none}.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul{display:block}.wy-menu-vertical li.toctree-l2.current>a{background:#c9c9c9;padding:0.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{display:block;background:#c9c9c9;padding:0.4045em 4.045em}.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l2 span.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3{font-size:0.9em}.wy-menu-vertical li.toctree-l3.current>a{background:#bdbdbd;padding:0.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{display:block;background:#bdbdbd;padding:0.4045em 5.663em;border-top:none;border-bottom:none}.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l3 span.toctree-expand{color:#969696}.wy-menu-vertical li.toctree-l4{font-size:0.9em}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical .local-toc li ul{display:block}.wy-menu-vertical li ul li a{margin-bottom:0;color:#b3b3b3;font-weight:normal}.wy-menu-vertical a{display:inline-block;line-height:18px;padding:0.4045em 1.618em;display:block;position:relative;font-size:90%;color:#b3b3b3}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover span.toctree-expand{color:#b3b3b3}.wy-menu-vertical a:active{background-color:#2980B9;cursor:pointer;color:#fff}.wy-menu-vertical a:active span.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:0.809em;margin-bottom:0.809em;z-index:200;background-color:#2980B9;text-align:center;padding:0.809em;display:block;color:#fcfcfc;margin-bottom:0.809em}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto 0.809em auto;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:0.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-side-nav-search>a img.logo,.wy-side-nav-search .wy-dropdown>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search>a.icon img.logo,.wy-side-nav-search .wy-dropdown>a.icon img.logo{margin-top:0.85em}.wy-side-nav-search>div.version{margin-top:-0.4045em;margin-bottom:0.809em;font-weight:normal;color:rgba(255,255,255,0.3)}.wy-nav .wy-menu-vertical header{color:#2980B9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980B9;color:#fff}[data-menu-wrap]{-webkit-transition:all 0.2s ease-in;-moz-transition:all 0.2s ease-in;transition:all 0.2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:left repeat-y #fcfcfc;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC);background-size:300px 1px}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980B9;color:#fff;padding:0.4045em 0.809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:#999}footer p{margin-bottom:12px}footer span.commit code,footer span.commit .rst-content tt,.rst-content footer span.commit tt{padding:0px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;font-size:1em;background:none;border:none;color:#999}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;font-size:90%}@media screen and (max-width: 768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-side-scroll{width:auto}.wy-side-nav-search{width:auto}.wy-menu.wy-menu-vertical{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width: 1400px){.wy-nav-content-wrap{background:rgba(0,0,0,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,footer,.wy-nav-side{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content p.caption .headerlink,.rst-content p.caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}}.rst-content img{max-width:100%;height:auto !important}.rst-content div.figure{margin-bottom:24px}.rst-content div.figure p.caption{font-style:italic}.rst-content div.figure.align-center{text-align:center}.rst-content .section>img,.rst-content .section>a>img{margin-bottom:24px}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last,.rst-content .admonition-todo .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha li{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .line-block{margin-left:24px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto;display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content .toctree-wrapper p.caption .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink{display:none;visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content .toctree-wrapper p.caption .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after,.rst-content p.caption .headerlink:after{visibility:visible;content:"";font-family:FontAwesome;display:inline-block}.rst-content h1:hover .headerlink,.rst-content h2:hover .headerlink,.rst-content .toctree-wrapper p.caption:hover .headerlink,.rst-content h3:hover .headerlink,.rst-content h4:hover .headerlink,.rst-content h5:hover .headerlink,.rst-content h6:hover .headerlink,.rst-content dl dt:hover .headerlink,.rst-content p.caption:hover .headerlink{display:inline-block}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#F1C40F;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:super;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:#999}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.docutils.citation tt,.rst-content table.docutils.citation code,.rst-content table.docutils.footnote tt,.rst-content table.docutils.footnote code{color:#555}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none;padding-top:5px}.rst-content table.field-list td>strong{display:inline-block;margin-top:3px}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left;padding-left:0}.rst-content tt,.rst-content tt,.rst-content code{color:#000;padding:2px 5px}.rst-content tt big,.rst-content tt em,.rst-content tt big,.rst-content code big,.rst-content tt em,.rst-content code em{font-size:100% !important;line-height:normal}.rst-content tt.literal,.rst-content tt.literal,.rst-content code.literal{color:#E74C3C}.rst-content tt.xref,a .rst-content tt,.rst-content tt.xref,.rst-content code.xref,a .rst-content tt,a .rst-content code{font-weight:bold;color:#404040}.rst-content a tt,.rst-content a tt,.rst-content a code{color:#2980B9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:inline-block;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980B9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:#555}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) code{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) code.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{display:inline-block;color:#27AE60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:bold}.rst-content tt.download,.rst-content code.download{background:inherit;padding:inherit;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{margin-right:4px}@media screen and (max-width: 480px){.rst-content .sidebar{width:100%}}span[id*='MathJax-Span']{color:#404040}.math{text-align:center}@font-face{font-family:"Inconsolata";font-style:normal;font-weight:400;src:local("Inconsolata"),local("Inconsolata-Regular"),url(../fonts/Inconsolata-Regular.ttf) format("truetype")}@font-face{font-family:"Inconsolata";font-style:normal;font-weight:700;src:local("Inconsolata Bold"),local("Inconsolata-Bold"),url(../fonts/Inconsolata-Bold.ttf) format("truetype")}@font-face{font-family:"Lato";font-style:normal;font-weight:400;src:local("Lato Regular"),local("Lato-Regular"),url(../fonts/Lato-Regular.ttf) format("truetype")}@font-face{font-family:"Lato";font-style:normal;font-weight:700;src:local("Lato Bold"),local("Lato-Bold"),url(../fonts/Lato-Bold.ttf) format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:400;src:local("Roboto Slab Regular"),local("RobotoSlab-Regular"),url(../fonts/RobotoSlab-Regular.ttf) format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:700;src:local("Roboto Slab Bold"),local("RobotoSlab-Bold"),url(../fonts/RobotoSlab-Bold.ttf) format("truetype")} +/*# sourceMappingURL=theme.css.map */ diff --git a/doc/build/html/_static/fonts/Inconsolata-Bold.ttf b/doc/build/html/_static/fonts/Inconsolata-Bold.ttf new file mode 100644 index 0000000..58c9fef Binary files /dev/null and b/doc/build/html/_static/fonts/Inconsolata-Bold.ttf differ diff --git a/doc/build/html/_static/fonts/Inconsolata-Regular.ttf b/doc/build/html/_static/fonts/Inconsolata-Regular.ttf new file mode 100644 index 0000000..a87ffba Binary files /dev/null and b/doc/build/html/_static/fonts/Inconsolata-Regular.ttf differ diff --git a/doc/build/html/_static/fonts/Lato-Bold.ttf b/doc/build/html/_static/fonts/Lato-Bold.ttf new file mode 100644 index 0000000..7434369 Binary files /dev/null and b/doc/build/html/_static/fonts/Lato-Bold.ttf differ diff --git a/doc/build/html/_static/fonts/Lato-Regular.ttf b/doc/build/html/_static/fonts/Lato-Regular.ttf new file mode 100644 index 0000000..04ea8ef Binary files /dev/null and b/doc/build/html/_static/fonts/Lato-Regular.ttf differ diff --git a/doc/build/html/_static/fonts/RobotoSlab-Bold.ttf b/doc/build/html/_static/fonts/RobotoSlab-Bold.ttf new file mode 100644 index 0000000..df5d1df Binary files /dev/null and b/doc/build/html/_static/fonts/RobotoSlab-Bold.ttf differ diff --git a/doc/build/html/_static/fonts/RobotoSlab-Regular.ttf b/doc/build/html/_static/fonts/RobotoSlab-Regular.ttf new file mode 100644 index 0000000..eb52a79 Binary files /dev/null and b/doc/build/html/_static/fonts/RobotoSlab-Regular.ttf differ diff --git a/doc/build/html/_static/fonts/fontawesome-webfont.eot b/doc/build/html/_static/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..84677bc Binary files /dev/null and b/doc/build/html/_static/fonts/fontawesome-webfont.eot differ diff --git a/doc/build/html/_static/fonts/fontawesome-webfont.svg b/doc/build/html/_static/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..d907b25 --- /dev/null +++ b/doc/build/html/_static/fonts/fontawesome-webfont.svg @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/_static/fonts/fontawesome-webfont.ttf b/doc/build/html/_static/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..96a3639 Binary files /dev/null and b/doc/build/html/_static/fonts/fontawesome-webfont.ttf differ diff --git a/doc/build/html/_static/fonts/fontawesome-webfont.woff b/doc/build/html/_static/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..628b6a5 Binary files /dev/null and b/doc/build/html/_static/fonts/fontawesome-webfont.woff differ diff --git a/doc/build/html/_static/js/modernizr.min.js b/doc/build/html/_static/js/modernizr.min.js new file mode 100644 index 0000000..f65d479 --- /dev/null +++ b/doc/build/html/_static/js/modernizr.min.js @@ -0,0 +1,4 @@ +/* Modernizr 2.6.2 (Custom Build) | MIT & BSD + * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load + */ +;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(["#",h,"{font:0/0 a}#",h,':after{content:"',l,'";visibility:hidden;font:3px/1 a}'].join(""),function(b){a=b.offsetHeight>=3}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var L in s)C(s,L)&&(x=L.toLowerCase(),e[x]=s[L](),v.push((e[x]?"":"no-")+x));return e.input||K(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},D(""),i=k=null,function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f"); + + // Add expand links to all parents of nested ul + $('.wy-menu-vertical ul').not('.simple').siblings('a').each(function () { + var link = $(this); + expand = $(''); + expand.on('click', function (ev) { + self.toggleCurrent(link); + ev.stopPropagation(); + return false; + }); + link.prepend(expand); + }); + }; + + nav.reset = function () { + // Get anchor from URL and open up nested nav + var anchor = encodeURI(window.location.hash); + if (anchor) { + try { + var link = $('.wy-menu-vertical') + .find('[href="' + anchor + '"]'); + $('.wy-menu-vertical li.toctree-l1 li.current') + .removeClass('current'); + link.closest('li.toctree-l2').addClass('current'); + link.closest('li.toctree-l3').addClass('current'); + link.closest('li.toctree-l4').addClass('current'); + } + catch (err) { + console.log("Error expanding nav for anchor", err); + } + } + }; + + nav.onScroll = function () { + this.winScroll = false; + var newWinPosition = this.win.scrollTop(), + winBottom = newWinPosition + this.winHeight, + navPosition = this.navBar.scrollTop(), + newNavPosition = navPosition + (newWinPosition - this.winPosition); + if (newWinPosition < 0 || winBottom > this.docHeight) { + return; + } + this.navBar.scrollTop(newNavPosition); + this.winPosition = newWinPosition; + }; + + nav.onResize = function () { + this.winResize = false; + this.winHeight = this.win.height(); + this.docHeight = $(document).height(); + }; + + nav.hashChange = function () { + this.linkScroll = true; + this.win.one('hashchange', function () { + this.linkScroll = false; + }); + }; + + nav.toggleCurrent = function (elem) { + var parent_li = elem.closest('li'); + parent_li.siblings('li.current').removeClass('current'); + parent_li.siblings().find('li.current').removeClass('current'); + parent_li.find('> ul li.current').removeClass('current'); + parent_li.toggleClass('current'); + } + + return nav; +}; + +module.exports.ThemeNav = ThemeNav(); + +if (typeof(window) != 'undefined') { + window.SphinxRtdTheme = { StickyNav: module.exports.ThemeNav }; +} + +},{"jquery":"jquery"}]},{},["sphinx-rtd-theme"]); diff --git a/doc/build/html/genindex.html b/doc/build/html/genindex.html index 6600ab6..ca79c9e 100644 --- a/doc/build/html/genindex.html +++ b/doc/build/html/genindex.html @@ -1,46 +1,147 @@ - - - - - - Index — pyral 1.1.1 documentation + + + + + + + + + Index — pyral 1.2.0 documentation + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + +
+ - - - - - - - - - -
-
-
-
+ + +
+ + + + + + +
+
+ + + + + + + + + + +
+
    +
  • Docs »
  • + +
  • +
  • + + + +
  • +
+
+
+
+

Index

@@ -154,12 +255,16 @@

G

-
getProject() +
getCollection()
+
getProject() +
+ +
getProjects()
@@ -219,10 +324,26 @@

R

Rally (built-in class)
+ +
RallyRESTResponse (built-in class) +
+ + +
rankAbove() +
+
-
RallyRESTResponse (built-in class) +
rankBelow() +
+ + +
rankBottom() +
+ + +
rankTop()
@@ -274,43 +395,62 @@

U

+
-
-
-
-
+
+ - +
+ +
+

+ © Copyright Rally Software Development 2010-2015, CA Technologies 2016. -

- +
+ Built with Sphinx using a theme provided by Read the Docs. + +
+
-
-
- - - + + + +
+ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/index.html b/doc/build/html/index.html index e498051..3eb8190 100644 --- a/doc/build/html/index.html +++ b/doc/build/html/index.html @@ -1,49 +1,149 @@ - - - - - - Python toolkit for the Rally REST API — pyral 1.1.1 documentation + + + + + + + + + Python toolkit for the Rally REST API — pyral 1.2.0 documentation + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + +
+ - - - - - - - - - - -
-
-
-
+ + +
+ + + + + + +
+
+ + + + + + + + + + +
+ +
+
+
+

Python toolkit for the Rally REST API

@@ -62,8 +162,8 @@

Python toolkit for the Rally REST APICustom Fields
  • Introduction of Dyna-Types
  • Primary pyral classes and functions
  • -
  • rallySettings
  • rallyWorkset
  • +
  • rallySettings
  • Rally
  • -
    -
    - - - + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/interface.html b/doc/build/html/interface.html index d263905..042925f 100644 --- a/doc/build/html/interface.html +++ b/doc/build/html/interface.html @@ -1,49 +1,153 @@ - - - - - - Primary pyral classes and functions — pyral 1.1.1 documentation + + + + + + + + + Primary pyral classes and functions — pyral 1.2.0 documentation + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + +
    + - - - - - - - - - - -
    -
    -
    -
    + + +
    + + + + + + +
    +
    + + + + + + + + + + +
    + +
    +
    +
    +

    Primary pyral classes and functions

    @@ -61,19 +165,21 @@

    Primary pyral classes and functions -

    rallySettings

    +
    +

    rallyWorkset

    +

    New in 1.1.0.

    This function takes into account your environment and arguments provided to this function to arrive at and return information necessary to establish a useful connection to the -Rally server.

    +Rally server. This convenience function differs from rallySettings by also including +the Rally API Key in the set of values considered and returned.

    The process consists of a priority chain where some reasonable default information is established first and then overridden with subsequent steps in the chain (if they exist). -After following the priority chain, values for server, user, password, workspace, project -are returned to the caller.

    +After following the priority chain, values for server, user, password, apikey, workspace, +project are returned to the caller.

    The priority chain consists of these steps:
      -
    • establish baseline values from values defined in the module containing the rallySettings

      +
    • establish baseline values from values defined in the module containing the rallyWorkset

    • override with any environment variables present from this list:
      @@ -81,8 +187,10 @@

      rallySettings—rallyPassword=<validRallyPassword>

    -

    –workspace=<validWorkspaceName>

    -

    —project=<validProjectName>

    +

    –apikey=<validRallyAPIKeyValue>

    +

    —workspace=<validWorkspaceName>

    +
    +
    +

    –project=<validProjectName>

    +

    —ping=True|False|true|false|yes|no|1|0

    This mechanism provides the ability to centrally locate a configuration file that can @@ -146,6 +260,14 @@

    rallySettingshttps://rally1.rallydev.com/login. +Once obtained, you should treat the key with the same level of protection as you would +any user/password information; once presented to Rally via the Rally Web Services API, +a connection has all the rights associated with the user whose key was presented. +Consult the Rally help documentation for further information.

    Example use:

    % export RALLY_SERVER="rally1.rallydev.com"
     % export RALLY_USER="crazedwiley@acmeproducts.com"
    @@ -163,37 +285,37 @@ 

    rallySettings -

    rallyWorkset

    -

    New in 1.1.0.

    +
    +

    rallySettings

    +

    This is deprecated as of v1.2.0. The preferred function is rallyWorkset which will have +ongoing support. The rallySettings function will be removed in v2.0.0.

    This function takes into account your environment and arguments provided to this function to arrive at and return information necessary to establish a useful connection to the -Rally server. This convenience function differs from rallySettings by also including -the Rally API Key in the set of values considered and returned.

    +Rally server.

    The process consists of a priority chain where some reasonable default information is established first and then overridden with subsequent steps in the chain (if they exist). -After following the priority chain, values for server, user, password, apikey, workspace, -project are returned to the caller.

    +After following the priority chain, values for server, user, password, workspace, project +are returned to the caller.

    The priority chain consists of these steps:
      -
    • establish baseline values from values defined in the module containing the rallyWorkset

      +
    • establish baseline values from values defined in the module containing the rallySettings

    • override with any environment variables present from this list:
      @@ -201,7 +323,6 @@

      rallyWorkset

    +
  • +
    server_ping (True or False, default in v1.2.0 is True)
    +

    Specifies whether a ping attempt will be made to confirm network connectivity +to the Rally server prior to making a Rally WSAPI REST request. +Organizations may have disabled the ability to make ICMP requests so the ping +attempt may fail even though there is network connectivity to the Rally server. +For this reason, the use of the ping=True option is discouraged going forward. +The next minor point release of pyral (v1.3.0) will have the default value +for this option inverted to be False. +The the ping operation itself will be dropped in the next major release (2.0.0).

    +
    +
    +
  • +
  • +
    isolated_workspace (True or False, default in v1.2.0 is False)
    +

    Specifies that the Rally instance will only be used for interacting with +a single workspace (either the user’s default workspace or the named workspace). +Using isolated_workspace=True provides performance benefits for a subscription +with many workspaces, but it also means you cannot change the workspace you +are working within a single instance of a Rally class, nor can you provide +a workspace keyword argument to a get, create, update or delete methods that +differs from the workspace identified at instantiation time. +For subscriptions with a small to moderate number of workspaces (up to a few dozen), +the performance savings will be relatively minor when using isolated_workspace=True +vs. isolated_workspace=False. However, for subscriptions with a large number of +workspaces, using isolated_workspace=False results in a request to AgileCentral +for each workspace, which can result in a noticeable lag before the instantiation +statement returns a ready-for-use Rally instance.

    +
    +
    +
  • @@ -363,7 +515,7 @@

    Rally

    -class Rally(server, user=None, password=None, apikey=None, workspace=None, project=None, warn=True)
    +class Rally(server, user=None, password=None, apikey=None, workspace=None, project=None, warn=True, server_ping=True)

    Examples:

    @@ -376,6 +528,8 @@

    Rally rally = Rally(server, user, password, workspace='Brontoville', verify_ssl_cert=False, warn=False) rally = Rally(server, apikey="_some-more-numbers", workspace='RockLobster', project='Fence Posts') + +rally = Rally('rally1.rallydev.com', 'chester@corral.com', 'bAbYF@cerZ', server_ping=False)

    @@ -618,7 +772,10 @@

    pyral.Rally instance convenience methodsgetAllUsers(workspace=None)

    This method offers a convenient one-stop means of obtaining usable information about all users in the named workspace. -If no workspace is specified, then the current context’s workspace is used.

    +If no workspace is specified, then the current context’s workspace is used. +NOTE: Unless you are using credentials associated with a SubscriptionAdministrator +or WorkspaceAdministrator, you will not be able to access a user’s UserProfile +other than yourself.

    Return a list of User instances (fully hydrated for scalar attributes) whose ref and collection attributes will be lazy eval’ed upon access.

    @@ -633,6 +790,16 @@

    pyral.Rally instance convenience methods +
    +getCollection(collection_url)
    +

    Given a collection_url of the form:

    +
    +
    http(s)://<server>(:<port>)/slm/webservice/v2.0/<entity>/OID/<attribute>
    +

    issue a request for the url and return back a list of hydrated instances +for each item in the collection.

    +
    +
    getState(entityName, stateName)
    @@ -709,6 +876,32 @@

    pyral.Rally instance convenience methods +
    +rankAbove(reference_artifact, target_artifact)
    +

    Rank the target_artifact above the reference_artifact.

    +

    + +
    +
    +rankBelow(reference_artifact, target_artifact)
    +

    Rank the target_artifact below the reference_artifact.

    +
    + +
    +
    +rankTop(target_artifact)
    +

    Rank the target_artifact at the top of the list of ranked Artifacts +that the target_artifact exists in.

    +
    + +
    +
    +rankBottom(target_artifact)
    +

    Rank the target_artifact at the bottom of the list of ranked Artifacts +that the target_artifact exists in.

    +
    +

    @@ -772,7 +965,7 @@

    Item AttributesRally WS API.

    +Rally WS API.

    Generally, every concrete instance in the Rally system will have a Name attribute. You can use the attributes() method on an instance to obtain the names of all of the @@ -815,7 +1008,7 @@

    Item Attributes -
    -

    Table Of Contents

    - + +

    -
    -
    - - - + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/objects.inv b/doc/build/html/objects.inv index 4b490a6..9a60304 100644 Binary files a/doc/build/html/objects.inv and b/doc/build/html/objects.inv differ diff --git a/doc/build/html/overview.html b/doc/build/html/overview.html index 93f35b0..2810c0d 100644 --- a/doc/build/html/overview.html +++ b/doc/build/html/overview.html @@ -1,53 +1,150 @@ - - - - - - Python toolkit for the Rally REST API — pyral 1.1.1 documentation + + + + + + + + + Python toolkit for the Rally REST API — pyral 1.2.0 documentation + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + +
    + - - - - - - - - - - - -
    -
    -
    -
    + + +
    + + + + + + +
    +
    + + + + + + + + + + +
    + +
    +
    +
    +

    Python toolkit for the Rally REST API

    @@ -107,7 +204,7 @@

    Rally Entities and Artifacts -
    -

    Table Of Contents

    - + +

    -
    -
    - - - + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/search.html b/doc/build/html/search.html index e5b4430..107d261 100644 --- a/doc/build/html/search.html +++ b/doc/build/html/search.html @@ -1,99 +1,223 @@ - - - - - - Search — pyral 1.1.1 documentation - - - + + + + + + + + + Search — pyral 1.2.0 documentation + + + + + + + + + - - - - - - - + - + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + + + + + +
    +
    + + + + + + + + + + +
    +
      +
    • Docs »
    • + +
    • +
    • + +
    • +
    +
    +
    +
    +
    + + +
    +
    +
    + + +
    + +
    +

    + © Copyright Rally Software Development 2010-2015, CA Technologies 2016. + +

    +
    + Built with Sphinx using a theme provided by Read the Docs. + +
    +
    -
    -
    -
    -
    -
    -
    - - - + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/searchindex.js b/doc/build/html/searchindex.js index a647b67..5133715 100644 --- a/doc/build/html/searchindex.js +++ b/doc/build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({envversion:42,terms:{represent:[0,2],all:[0,2],code:0,partial:0,chain:0,whatev:0,attrget:0,queri:[],four:0,scalar:[0,2],abil:0,follow:0,perspect:2,children:0,hierarch:2,whose:[0,2],dairi:0,attachmentcont:0,friendli:2,concret:0,those:[0,2],under:0,introduc:2,adapt:2,everi:[0,2],string:[0,2],rally_us:0,fals:0,account:0,mime_typ:0,util:0,mechan:0,veri:2,join:0,relev:0,getattachmentnam:0,contenttyp:0,administr:0,level:0,gui:0,list:[0,2],signific:2,iter:[0,2],item:[],team:0,prevent:2,cfg:0,direct:0,uniqu:0,second:[0,2],further:[0,2],"0x101931290":0,append:0,even:0,index:[0,1,2],what:2,abc:0,foogelhorn:0,defin:[0,2],introspect:[],abl:0,find:0,access:[0,2],delet:[0,2],version:[0,2],"new":[0,2],method:[],metadata:0,full:[],abov:0,getproject:0,variat:0,gener:0,never:2,rallyconfig:0,here:2,bodi:0,let:[0,2],along:0,implicit:0,valu:[0,2],search:[0,1],produc:0,hydrat:0,base:0,acmeproduct:0,permit:[0,2],chang:0,cerz:0,defectstatu:0,overrid:0,story1:0,via:[0,2],elect:0,modul:[0,1],prefer:2,subscriptionnam:0,filenam:0,"boolean":0,instal:[1,2],total:0,establish:[0,2],select:0,from:[0,2],stream:0,would:[0,2],commun:2,portfolioitem:[0,2],visit:2,two:[0,2],todai:0,next:0,websit:2,call:[0,2],criteria:0,scope:0,type:[],minor:0,more:[0,2],sort:2,task1:0,somefield:0,relat:0,warn:0,prototyp:2,particular:0,validrallyusernam:0,actual:[0,1],compani:0,content_typ:0,must:0,none:0,retriev:[0,2],augment:2,alia:0,work:[0,2],focu:0,cat:0,itself:[0,2],can:[0,2],meet:0,purpos:0,fetch:[0,2],problemat:0,control:0,objectid:[0,2],startswith:[0,2],process:0,workspacedomain:0,backslash:0,accept:0,critic:0,tag:2,caution:0,want:[0,2],releasestartd:2,mainstream:2,filter:0,differenti:2,end:2,eng:0,anoth:[0,2],classifi:0,reset:0,divis:0,how:[0,2],opt:[0,2],instead:0,config:0,updat:[0,2],product:0,resourc:0,referenc:0,farm:0,after:[0,2],usabl:0,befor:0,outlandish:2,paren:0,date:2,wilei:0,data:[],getstat:0,alloc:2,attempt:0,save:0,ani:[0,2],rally_password:0,secur:0,credenti:[0,2],correspond:0,corral:0,issu:0,alias:[],"_x_":0,combin:0,allow:[0,2],volum:[0,2],order:[0,2],help:[0,2],pyral:[],kanbanst:0,blocker:0,offici:1,workspace_nam:0,through:2,"_ref":0,entiti:[],typedef:0,creationd:0,polici:0,better:0,validusernam:0,comprehens:0,main:[0,2],split:2,non:0,good:2,"return":[0,2],thei:0,handl:0,handi:0,initi:[0,2],mgmt:0,aka:0,now:2,discuss:0,introduct:[],term:2,document:[0,2],name:[0,1,2],simpl:[],authent:0,rally_workspac:0,separ:0,userprofil:2,each:[0,2],fulli:0,hijack:2,ifi:0,mean:[0,2],subset:2,recommend:0,individu:0,elementnam:2,meta:0,connect:0,energi:0,beyond:0,orient:2,special:0,baselin:0,variabl:0,overridden:0,space:0,newli:2,your:[0,2],content:[0,1],health:0,suitabl:0,"237z":0,print:[0,2],rec:0,cap_nam:0,ref:0,correct:0,multilin:0,attributenam:0,qualifi:0,oid:0,umbrella:0,burntofferingsindex:2,given:0,argv:[0,2],standard:0,reason:[0,2],addattach:0,validworkspacenam:0,dictionari:0,put:[0,2],care:[0,2],workaround:0,argument:0,could:0,projectnam:0,getallowedvalu:0,thing:2,like:0,first:[0,2],oper:[0,2],softwar:1,rang:2,suffix:0,directli:2,onc:[0,2],number:0,thereof:2,yourself:0,restrict:0,mai:[0,2],construct:0,oppos:[0,2],open:0,primari:[],mchunko:0,size:0,prioriti:0,differ:[0,2],convent:2,script:2,associ:[0,2],interact:[0,2],system:[0,2],least:0,fiction:2,attach:0,stori:[0,2],statement:2,similarli:0,conveni:[],prone:0,shell:0,option:[0,2],relationship:0,projectscopedown:0,specifi:[0,2],released:2,part:[0,2],dyna:[],consult:[0,2],than:0,kind:0,target:0,keyword:0,whenev:0,provid:0,charact:[0,2],project:[0,2],babyf:0,consumpt:0,toward:2,result:[],pre:2,sai:2,validapikei:0,accountingtask:0,saa:[0,2],respons:[0,2],defect:[0,2],packag:[0,1,2],rocklobst:0,have:[0,2],configfilenam:0,need:[0,2],rallypassword:0,ambigu:2,bogutrunk:2,inform:[0,2],latter:2,note:0,also:[0,2],exampl:[0,2],persistableobject:2,take:[0,2],which:[0,2],environ:0,singl:0,compat:2,importantli:0,rallyrestrespons:[],usernam:0,object:0,discov:0,most:[0,2],regular:0,mysek:0,artifact:[],pair:0,rallyrestexcept:0,pages:[0,2],interract:0,don:[0,2],url:[0,2],clear:0,request:[0,2],basicauth:0,doe:0,vernacular:2,chester:0,validpassword:0,affectedcustom:0,dot:[0,2],itemid:0,changeset:0,nutrit:0,text:0,whitelist:0,rallyus:0,syntax:[0,2],permiss:2,protocol:0,anywai:2,current:0,onli:0,locat:0,execut:0,fenc:0,writer:2,activ:0,lastupdated:0,should:0,configur:[0,2],theme:[0,2],dict:0,over:0,overwritten:0,info:0,ideavot:0,get:[0,2],lighten:2,express:0,stop:0,autom:0,sso:0,cannot:2,subscript:[0,2],requir:0,prime:0,getuserinfo:0,bar:0,enabl:[0,2],organ:0,wsapi:[0,2],whether:0,common:0,contain:[0,2],privileg:0,where:0,view:2,fieldnam:0,conform:0,set:[0,2],project_nam:0,sep:0,nut:0,displai:2,see:[0,2],story_titl:0,arg:0,testcas:[0,2],close:0,testset:2,kei:0,record:[0,2],someth:0,filbert:0,disablelog:0,state:0,testcaseresult:2,"098z":0,"import":[0,2],awai:2,paramet:0,attribut:[],altern:0,accord:2,syntact:0,parent:[0,2],numer:0,rally_config:0,rally1:0,s321:0,lazi:[0,2],entir:0,natur:0,entitynam:0,popul:0,"_hydrat":0,userstori:[0,2],protect:0,easi:[0,2],getallus:0,etc:0,instanc:[],present:[0,2],brontovil:0,context:0,logic:2,mani:0,login:[0,2],com:0,setproject:0,load:2,validrallyapikeyvalu:0,point:0,instanti:0,format:0,arriv:0,pop:0,header:0,suppli:[0,2],portfolioitemtyp:0,testfold:2,assum:0,quit:0,ultim:2,creat:[0,2],"_createdat":0,been:0,json:0,treat:[0,2],interest:[0,2],modif:2,unambigu:2,addit:0,strategi:[0,2],repons:0,spectrum:2,bulk:2,togeth:0,workspac:[0,2],turn:0,defectsuit:2,rallyproject:0,ident:[0,2],look:0,rlsstart:2,plain:0,servic:[0,2],typedefinit:0,slm:0,"while":2,apikei:[0,2],behavior:2,error:0,occas:0,exist:[0,2],observ:0,loop:[0,2],shamu:0,burdensom:2,advantag:2,stdout:0,them:[0,2],worri:2,destin:0,validprojectnam:0,kwarg:0,conf:0,tediou:0,sever:[0,2],chase:2,minim:0,alphabet:0,make:[0,2],belong:2,same:0,member:0,dest:0,thusli:0,decod:0,descend:2,resultcount:0,customfield:0,http:0,unwarr:2,upon:0,hand:0,capabl:[],acceptedd:0,user:[0,2],applic:0,typic:0,middlenam:0,task:[0,2],off:0,entri:0,well:[0,2],comma:0,know:0,without:2,command:0,thi:[0,1,2],comma_separated_list_of_field:0,model:[],startindex:0,usual:[0,2],explan:0,summer:0,identifi:[0,2],livestock:0,just:2,less:0,hierarchicalrequir:0,rallyset:[],obtain:[0,2],human:2,projectscopeup:0,yet:2,verify_ssl_cert:0,previous:0,web:0,prefix:2,cut:0,unsecur:0,character:2,except:0,identif:0,add:0,valid:[0,2],exercis:0,subsequ:0,match:0,real:0,rallyworkset:[],transpar:2,read:0,disposit:0,crud:[],intuit:2,setworkspac:0,characterist:2,burnt:2,itemdata:0,ditto:2,specif:[0,2],integ:0,server:[0,2],collect:0,rally_project:0,necessari:0,either:[0,2],scriptwrit:2,page:[0,1,2],right:0,deal:[0,2],stopiter:0,creation:0,some:[0,2],validrallypassword:0,"export":0,"_you_":0,consciou:0,proper:2,pain:0,inprogressd:0,formattedid:0,getworkspac:0,avoid:2,subclass:0,insul:2,track:2,equal:0,larg:0,slash:2,condit:0,refer:[0,2],core:[],encourag:2,accountingprojec:0,word:0,rally_serv:0,usag:[0,2],"852z":0,step:0,although:0,post:[0,2],sub:2,about:[0,2],central:0,"_some":0,acm:0,manag:0,addition:2,rallyserv:0,disabl:0,block:0,own:[0,2],consid:0,primarili:2,status_cod:0,within:0,appropri:0,capitaliz:0,getattach:0,ideaurl:0,empti:0,claus:2,mere:2,log:[0,2],wai:[0,2],aren:2,support:[0,2],"long":0,custom:[],avail:[0,2],start:0,interfac:2,includ:0,disambigu:2,chunk:0,"function":[],therefrom:2,form:0,offer:[0,2],tupl:0,crazedwilei:0,basic:0,jul:0,"12t09":0,link:[0,2],actualhour:0,line:0,"true":0,bug:2,count:0,notat:[0,2],statenam:0,consist:0,possibl:2,"default":0,workspacenam:0,caller:0,maximum:2,directori:0,below:0,limit:[0,2],rlsdate:2,enablelog:[0,2],featur:[0,2],displaynam:[0,2],evalu:2,"abstract":2,doesn:2,strongli:2,implement:2,file:0,encompass:2,ing:0,check:0,password:[0,2],when:0,detail:0,reduct:2,field:[],other:[0,2],branch:2,writabl:0,you:[0,2],out:2,servernam:0,tart:0,estimatedhour:0,nonscal:2,intend:0,sequenc:2,"class":[],releas:2,eval:0,rallydev:0,land:2,longer:0,mimetyp:0,webservic:[0,2],descript:0,portion:0,time:0,far:2,backward:2},objtypes:{"0":"py:method","1":"py:class"},objnames:{"0":["py","method","Python method"],"1":["py","class","Python class"]},filenames:["interface","index","overview"],titles:["Primary pyral classes and functions","Python toolkit for the Rally REST API","Python toolkit for the Rally REST API"],objects:{"":{getProject:[0,0,1,""],addAttachment:[0,0,1,""],enableLogging:[0,0,1,""],getAttachmentNames:[0,0,1,""],getWorkspaces:[0,0,1,""],getProjects:[0,0,1,""],find:[0,0,1,""],typedef:[0,0,1,""],create:[0,0,1,""],next:[0,0,1,""],getUserInfo:[0,0,1,""],details:[0,0,1,""],getAllUsers:[0,0,1,""],getAllowedValues:[0,0,1,""],getState:[0,0,1,""],get:[0,0,1,""],update:[0,0,1,""],getAttachment:[0,0,1,""],setWorkspace:[0,0,1,""],subscriptionName:[0,0,1,""],put:[0,0,1,""],post:[0,0,1,""],getStates:[0,0,1,""],setProject:[0,0,1,""],disableLogging:[0,0,1,""],search:[0,0,1,""],addAttachments:[0,0,1,""],getWorkspace:[0,0,1,""],getAttachments:[0,0,1,""],RallyRESTResponse:[0,1,1,""],Rally:[0,1,1,""],"delete":[0,0,1,""]}},titleterms:{pyral:0,ralli:[0,1,2],rallyset:0,toolkit:[1,2],rest:[0,1,2],api:[1,2],result:2,tabl:1,primari:0,entiti:2,alias:0,introspect:2,custom:2,field:2,queri:2,conveni:0,simpl:2,method:0,attribut:0,"function":0,rallyworkset:0,full:2,rallyrestrespons:0,python:[1,2],crud:[0,2],artifact:2,capabl:2,indic:1,dyna:2,core:0,data:2,"class":0,introduct:2,type:2,item:0,instanc:0,model:2}}) \ No newline at end of file +Search.setIndex({envversion:42,terms:{represent:[0,2],all:[0,2],code:0,partial:0,chain:0,whatev:0,attrget:0,queri:[0,1],criteria:0,four:0,scalar:[0,2],abil:0,follow:0,perspect:2,children:0,hierarch:2,whose:[0,2],dairi:0,attachmentcont:0,friendli:2,concret:0,defectsuit:2,under:0,introduc:2,adapt:2,everi:[0,2],string:[0,2],rally_us:0,fals:0,none:0,mime_typ:0,util:0,mechan:0,veri:2,retriev:[0,2],relev:0,getattachmentnam:0,contenttyp:0,administr:0,level:0,gui:0,list:[0,2],signific:2,iter:[0,2],workspaceadministr:0,item:1,team:0,small:0,prevent:2,cfg:0,oid:0,direct:0,uniqu:0,second:[0,2],further:[0,2],port:0,append:0,even:0,index:[0,1,2],what:2,abc:0,foogelhorn:0,defin:[0,2],introspect:1,abl:0,find:0,access:[0,2],delet:[0,2],version:[0,2],directori:0,"new":[0,2],method:1,metadata:0,ongo:0,full:1,abov:0,getproject:0,variat:0,gener:0,never:2,rallyconfig:0,here:2,bodi:0,let:[0,2],along:0,implicit:0,valu:[0,2],search:[0,1],hydrat:0,prior:0,base:0,acmeproduct:0,permit:[0,2],chang:0,cerz:0,defectstatu:0,overrid:0,story1:0,via:[0,2],elect:0,modul:[0,1],prefer:[0,2],subscriptionnam:0,put:[0,2],"boolean":0,rally_p:0,instal:[1,2],total:0,establish:[0,2],select:0,from:[0,2],stream:0,would:[0,2],commun:2,portfolioitem:[0,2],visit:2,two:[0,2],todai:0,next:0,websit:2,few:0,disambigu:2,recommend:0,scope:0,type:[0,1],minor:0,more:[0,2],sort:2,task1:0,ranktop:0,somefield:0,relat:0,notic:0,warn:0,prototyp:2,particular:0,validrallyusernam:0,actual:[0,1],compani:0,destin:0,content_typ:0,must:0,account:0,rallydev:0,join:0,augment:2,alia:0,work:[0,2],focu:0,cat:0,itself:[0,2],can:[0,2],meet:0,purpos:0,fetch:[0,2],problemat:0,control:0,objectid:[0,2],startswith:[0,2],process:0,workspacedomain:0,backslash:0,accept:0,critic:0,tag:2,caution:0,want:[0,2],releasestartd:2,mainstream:2,filter:0,differenti:2,end:2,secur:0,anoth:[0,2],ping:0,classifi:0,divis:0,how:[0,2],opt:[0,2],instead:0,config:0,updat:[0,2],product:0,resourc:0,overridden:0,farm:0,after:[0,2],usabl:0,befor:0,outlandish:2,paren:0,date:2,wilei:0,data:[0,1],getstat:0,isolated_workspac:0,alloc:2,attempt:0,save:0,ani:[0,2],rally_password:0,eng:0,credenti:[0,2],correspond:0,corral:0,issu:0,alias:1,"_x_":0,combin:0,allow:[0,2],volum:[0,2],order:[0,2],help:[0,2],pyral:1,kanbanst:0,blocker:0,offici:1,workspace_nam:0,major:0,through:2,"_ref":0,entiti:[0,1],typedef:0,creationd:0,polici:0,better:0,validusernam:0,comprehens:0,main:[0,2],split:2,prime:0,non:0,good:2,"return":[0,2],thei:0,handl:0,handi:0,initi:[0,2],rank:0,mgmt:0,aka:0,now:2,discuss:0,nor:0,introduct:1,term:2,document:[0,2],name:[0,1,2],simpl:[0,1],drop:0,authent:0,rally_workspac:0,separ:0,userprofil:[0,2],each:[0,2],fulli:0,hijack:2,ifi:0,mean:[0,2],subset:2,reset:0,individu:0,elementnam:2,meta:0,connect:0,energi:0,beyond:0,orient:2,special:0,baselin:0,variabl:0,network:0,space:0,newli:2,your:[0,2],content:[0,1],health:0,suitabl:0,rel:0,print:[0,2],rec:0,cap_nam:0,ref:0,correct:0,multilin:0,prioriti:0,qualifi:0,lag:0,umbrella:0,burntofferingsindex:2,given:0,argv:[0,2],standard:0,reason:[0,2],addattach:0,validworkspacenam:0,dictionari:0,releas:[0,2],care:[0,2],workaround:0,argument:0,could:0,script:2,getallowedvalu:0,turn:0,interact:[0,2],first:[0,2],oper:[0,2],softwar:1,rang:2,suffix:0,directli:2,onc:[0,2],number:0,thereof:2,yourself:0,restrict:0,mai:[0,2],least:0,rallyworkset:1,oppos:[0,2],custom:1,open:0,primari:1,mchunko:0,size:0,attributenam:0,avail:[0,2],differ:[0,2],convent:2,projectnam:0,associ:[0,2],top:0,system:[0,2],construct:0,fiction:2,attach:0,startindex:0,statement:[0,2],similarli:0,conveni:1,prone:0,shell:0,option:[0,2],relationship:0,projectscopedown:0,specifi:[0,2],released:2,part:[0,2],dyna:1,consult:[0,2],moder:0,than:0,kind:0,target:0,keyword:0,whenev:0,provid:0,remov:0,charact:[0,2],project:[0,2],babyf:0,consumpt:0,stori:[0,2],result:[0,1],pre:2,sai:2,validapikei:0,accountingtask:0,saa:[0,2],respons:[0,2],deprec:0,defect:[0,2],packag:[0,1,2],rocklobst:0,have:[0,2],configfilenam:0,need:[0,2],rallypassword:0,ambigu:2,dozen:0,bogutrunk:2,inform:[0,2],latter:2,webservic:[0,2],"0x101931290":0,note:0,also:[0,2],exampl:[0,2],persistableobject:2,take:[0,2],which:[0,2],environ:0,singl:0,compat:2,importantli:0,rallyrestrespons:1,though:0,usernam:0,object:0,discov:0,most:[0,2],regular:0,mysek:0,artifact:[0,1],pair:0,rallyrestexcept:0,pages:[0,2],interract:0,don:[0,2],url:[0,2],clear:0,request:[0,2],basicauth:0,doe:0,vernacular:2,chester:0,validpassword:0,affectedcustom:0,dot:[0,2],itemid:0,changeset:0,nutrit:0,text:0,whitelist:0,rallyus:0,syntax:[0,2],permiss:2,identifi:[0,2],anywai:2,current:0,onli:0,locat:0,execut:0,fenc:0,writer:2,activ:0,lastupdated:0,should:0,configur:[0,2],theme:[0,2],dict:0,over:0,overwritten:0,info:0,ideavot:0,get:[0,2],lighten:2,express:0,stop:0,autom:0,sso:0,cannot:[0,2],subscript:[0,2],requir:0,collection_url:0,getuserinfo:0,bar:0,enabl:[0,2],organ:0,wsapi:[0,2],whether:0,common:0,contain:[0,2],privileg:0,where:0,view:2,fieldnam:0,conform:0,set:[0,2],project_nam:0,sep:0,nut:0,displai:2,see:[0,2],story_titl:0,arg:0,testcas:[0,2],close:0,testset:2,kei:0,someth:0,filbert:0,disablelog:0,state:0,testcaseresult:2,"098z":0,"import":[0,2],awai:2,paramet:0,attribut:1,altern:0,accord:2,syntact:0,parent:[0,2],numer:0,rally_config:0,rally1:0,s321:0,lazi:[0,2],entir:0,natur:0,entitynam:0,popul:0,"_hydrat":0,userstori:[0,2],protect:0,toward:2,easi:[0,2],howev:0,getallus:0,etc:0,instanc:1,brontovil:0,context:0,logic:2,mani:0,login:[0,2],com:0,setproject:0,load:2,validrallyapikeyvalu:0,point:0,instanti:0,format:0,arriv:0,pop:0,header:0,suppli:[0,2],portfolioitemtyp:0,testfold:2,assum:0,rankbottom:0,quit:0,ultim:2,creat:[0,2],addition:2,invert:0,rankbelow:0,been:0,agilecentr:0,treat:[0,2],interest:[0,2],modif:2,unambigu:2,addit:0,strategi:[0,2],repons:0,spectrum:2,bulk:2,togeth:0,workspac:[0,2],thing:2,present:[0,2],rallyproject:0,ident:[0,2],look:0,rlsstart:2,plain:0,servic:[0,2],subscriptionadministr:0,typedefinit:0,slm:0,"while":2,apikei:[0,2],behavior:2,error:0,occas:0,exist:[0,2],observ:0,loop:[0,2],shamu:0,burdensom:2,advantag:2,stdout:0,readi:0,them:[0,2],worri:2,equal:0,validprojectnam:0,kwarg:0,conf:0,limit:[0,2],tediou:0,sever:[0,2],chase:2,disabl:0,minim:0,perform:0,alphabet:0,make:[0,2],belong:2,same:0,member:0,dest:0,thusli:0,decod:0,descend:2,resultcount:0,customfield:0,http:0,referenc:0,unwarr:2,upon:0,hand:0,capabl:1,acceptedd:0,user:[0,2],typic:0,task:[0,2],off:0,discourag:0,entri:0,target_artifact:0,well:[0,2],comma:0,know:0,without:2,command:0,thi:[0,1,2],comma_separated_list_of_field:0,fail:0,usual:[0,2],explan:0,summer:0,protocol:0,livestock:0,just:2,less:0,hierarchicalrequir:0,rallyset:1,obtain:[0,2],server_p:0,human:2,projectscopeup:0,yet:2,verify_ssl_cert:0,previous:0,web:0,prefix:2,cut:0,unsecur:0,character:2,except:0,identif:0,add:0,valid:[0,2],exercis:0,subsequ:0,match:0,real:0,applic:0,transpar:2,read:0,disposit:0,crud:1,intuit:2,setworkspac:0,characterist:2,burnt:2,itemdata:0,ditto:2,specif:[0,2],filenam:0,rankabov:0,integ:0,server:[0,2],collect:0,benefit:0,rally_project:0,necessari:0,either:[0,2],scriptwrit:2,page:[0,1,2],reference_artifact:0,right:0,deal:[0,2],stopiter:0,creation:0,some:[0,2],back:0,validrallypassword:0,"export":0,"_you_":0,consciou:0,proper:2,pain:0,inprogressd:0,formattedid:0,confirm:0,getworkspac:0,avoid:2,subclass:0,insul:2,track:2,larg:0,slash:2,condit:0,refer:[0,2],core:1,encourag:2,accountingprojec:0,word:0,usag:[0,2],"852z":0,step:0,although:0,post:[0,2],sub:2,about:[0,2],central:0,"_some":0,acm:0,manag:0,"_createdat":0,rallyserv:0,produc:0,block:0,own:[0,2],consid:0,primarili:2,status_cod:0,within:0,appropri:0,capitaliz:0,getattach:0,ideaurl:0,empti:0,claus:2,mere:2,log:[0,2],wai:[0,2],aren:2,support:[0,2],json:0,"long":0,rally_serv:0,like:0,start:0,interfac:2,includ:0,forward:0,call:[0,2],chunk:0,"function":1,therefrom:2,form:0,offer:[0,2],tupl:0,crazedwilei:0,basic:0,jul:0,"12t09":0,link:[0,2],actualhour:0,"237z":0,line:0,icmp:0,"true":0,bug:2,count:0,notat:[0,2],made:0,statenam:0,consist:0,possibl:2,"default":0,workspacenam:0,caller:0,maximum:2,record:[0,2],below:0,those:[0,2],rlsdate:2,enablelog:[0,2],model:1,featur:[0,2],displaynam:[0,2],evalu:2,"abstract":2,doesn:2,strongli:2,implement:2,file:0,encompass:2,ing:0,check:0,password:[0,2],when:0,detail:0,intend:0,reduct:2,field:[0,1],other:[0,2],branch:2,writabl:0,you:[0,2],out:2,servernam:0,tart:0,estimatedhour:0,nonscal:2,middlenam:0,sequenc:2,"class":1,eval:0,unless:0,getcollect:0,land:2,longer:0,mimetyp:0,bottom:0,descript:0,portion:0,time:0,far:2,backward:2},objtypes:{"0":"py:method","1":"py:class"},objnames:{"0":["py","method","Python method"],"1":["py","class","Python class"]},filenames:["interface","index","overview"],titles:["Primary pyral classes and functions","Python toolkit for the Rally REST API","Python toolkit for the Rally REST API"],objects:{"":{getProject:[0,0,1,""],addAttachment:[0,0,1,""],rankBelow:[0,0,1,""],search:[0,0,1,""],getCollection:[0,0,1,""],getState:[0,0,1,""],getWorkspaces:[0,0,1,""],getProjects:[0,0,1,""],find:[0,0,1,""],typedef:[0,0,1,""],rankTop:[0,0,1,""],create:[0,0,1,""],rankAbove:[0,0,1,""],next:[0,0,1,""],details:[0,0,1,""],getAllUsers:[0,0,1,""],getAllowedValues:[0,0,1,""],getAttachmentNames:[0,0,1,""],get:[0,0,1,""],update:[0,0,1,""],getAttachment:[0,0,1,""],enableLogging:[0,0,1,""],setWorkspace:[0,0,1,""],subscriptionName:[0,0,1,""],put:[0,0,1,""],post:[0,0,1,""],getStates:[0,0,1,""],setProject:[0,0,1,""],disableLogging:[0,0,1,""],getUserInfo:[0,0,1,""],addAttachments:[0,0,1,""],getWorkspace:[0,0,1,""],getAttachments:[0,0,1,""],RallyRESTResponse:[0,1,1,""],Rally:[0,1,1,""],rankBottom:[0,0,1,""],"delete":[0,0,1,""]}},titleterms:{pyral:0,ralli:[0,1,2],rallyset:0,toolkit:[1,2],rest:[0,1,2],api:[1,2],result:2,tabl:1,primari:0,entiti:2,alias:0,introspect:2,custom:2,field:2,queri:2,conveni:0,simpl:2,method:0,attribut:0,"function":0,rallyworkset:0,full:2,rallyrestrespons:0,python:[1,2],crud:[0,2],artifact:2,capabl:2,indic:1,dyna:2,core:0,data:2,"class":0,introduct:2,type:2,item:0,instanc:0,model:2}}) \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py index c1306b1..74b8ccf 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -40,17 +40,17 @@ master_doc = 'index' # General information about the project. -project = u'pyral' -copyright = u'Rally Software Development 2010-2015' +project = 'pyral' +copyright = 'Rally Software Development 2010-2015, CA Technologies 2016' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.1.1' +version = '1.2.0' # The full version, including alpha/beta/rc tags. -release = '1.1.1' +release = '1.2.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -101,6 +101,16 @@ # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] +# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# otherwise, readthedocs.org uses their theme by default, so no need to specify it + # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None @@ -178,7 +188,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'pyral.tex', u'pyral Documentation', u'Rally Software Developement', 'manual'), + ('index', 'pyral.tex', 'pyral Documentation', 'CA Technologies', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -210,5 +220,5 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'pyral', u'pyral Documentation', [u'Rally Software Development'], 1) + ('index', 'pyral', 'pyral Documentation', ['CA Technologies'], 1) ] diff --git a/doc/source/interface.rst b/doc/source/interface.rst index 93ddd68..4a492d7 100644 --- a/doc/source/interface.rst +++ b/doc/source/interface.rst @@ -18,26 +18,31 @@ of the **RallyRESTResponse** class. Instances of this class allow easy dot ('.' access to attributes of the representation of the Rally entity, whether the attribute is a simple value or a reference to another Rally entity. -rallySettings -============= +rallyWorkset +============ + +New in 1.1.0. This function takes into account your environment and arguments provided to this function to arrive at and return information necessary to establish a useful *connection* to the -Rally server. +Rally server. This convenience function differs from **rallySettings** by also including +the Rally API Key in the set of values considered and returned. The process consists of a priority chain where some reasonable default information is established first and then overridden with subsequent steps in the chain (if they exist). -After following the priority chain, values for server, user, password, workspace, project -are returned to the caller. +After following the priority chain, values for server, user, password, apikey, workspace, +project are returned to the caller. The priority chain consists of these steps: - * establish baseline values from values defined in the module containing the rallySettings + * establish baseline values from values defined in the module containing the rallyWorkset * override with any environment variables present from this list: - RALLY_SERVER - RALLY_USER - RALLY_PASSWORD + - APIKEY - RALLY_WORKSPACE - RALLY_PROJECT + - RALLY_PING * if present, use information from a rally-.cfg file in the current directory, where matches the Rally WSAPI version defined in the pyral.config module. Currently, that version is defined as v2.0. @@ -57,8 +62,10 @@ The specific syntax available for these levels is detailed below. - SERVER = - USER = - PASSWORD = + - APIKEY = - WORKSPACE = - PROJECT = + - RALLY_PING = True | False **Command line options** @@ -76,9 +83,13 @@ The specific syntax available for these levels is detailed below. --rallyPassword= + --apikey= + --workspace= --project= + + --ping=True|False|true|false|yes|no|1|0 This mechanism provides the ability to centrally locate a configuration file that can be used by many members of a team where server, workspace, project are common to all members @@ -86,6 +97,15 @@ and each individual can have their own appropriately secured config file with th Using this mechanism can save tedious and error-prone entry of target information and credentials on the command line or having credential information in clear text in unsecured files. +The use of a Rally API Key value for identification/authentication is new in pyral 1.1.x. +If used, you do not need to provide a username / password combination. +In order to use this, you must first obtain a valid API Key value from the Rally Application +Manager (API Keys) that you can access from https://rally1.rallydev.com/login. +Once obtained, you should treat the key with the same level of protection as you would +any user/password information; once presented to Rally via the Rally Web Services API, +a connection has all the rights associated with the user whose key was presented. +Consult the Rally help documentation for further information. + Example use:: % export RALLY_SERVER="rally1.rallydev.com" @@ -104,45 +124,44 @@ Example use:: % cat basic.py import sys - from rally import rallySettings + from rally import rallyWorkset options = [opt for opt in sys.argv[1:] if opt.startswith('--')] - server, user, password, workspace, project = rallySettings(options) - print " ".join(['|%|' % opt for opt in [server, user, password, workspace, project]] + server, user, password, apikey, workspace, project = rallyWorkset(options) + print " ".join(['|%|' % opt for opt in [server, user, password, apikey, workspace, project]] - % python basic.py --config=current --rallyPassword='*****' --rallyProject="Livestock Mgmt" + % python basic.py --config=current --rallyProject="Livestock Mgmt" --ping=no - |rally1.rallydev.com| |wiley@acme.com| |*****| |General Products Umbrella| |Livestock Mgmt| + |rally1.rallydev.com| |wiley@acme.com| |*****| |*****| |General Products Umbrella| |Livestock Mgmt| Note that for convenience purposes a configuration file name may be fully specified or you may elect to not specify the '.cfg' suffix. -Returns a tuple of (server, username, password, workspace, project) +Returns a tuple of (server, username, password, apikey, workspace, project) -rallyWorkset -============ +rallySettings +============= -New in 1.1.0. +This is deprecated as of v1.2.0. The preferred function is **rallyWorkset** which will have +ongoing support. The **rallySettings** function will be removed in v2.0.0. This function takes into account your environment and arguments provided to this function to arrive at and return information necessary to establish a useful *connection* to the -Rally server. This convenience function differs from **rallySettings** by also including -the Rally API Key in the set of values considered and returned. +Rally server. The process consists of a priority chain where some reasonable default information is established first and then overridden with subsequent steps in the chain (if they exist). -After following the priority chain, values for server, user, password, apikey, workspace, -project are returned to the caller. +After following the priority chain, values for server, user, password, workspace, project +are returned to the caller. The priority chain consists of these steps: - * establish baseline values from values defined in the module containing the rallyWorkset + * establish baseline values from values defined in the module containing the rallySettings * override with any environment variables present from this list: - RALLY_SERVER - RALLY_USER - RALLY_PASSWORD - - APIKEY - RALLY_WORKSPACE - RALLY_PROJECT * if present, use information from a rally-.cfg file in the current directory, @@ -264,6 +283,29 @@ Rally and the default project for the user is not in the workspace specified. Under those conditions, the project is changed to the first project (alphabetic ordering) in the list of projects for the specified workspace. + * server_ping (True or False, default in v1.2.0 is True) + Specifies whether a ping attempt will be made to confirm network connectivity + to the Rally server prior to making a Rally WSAPI REST request. + Organizations may have disabled the ability to make ICMP requests so the ping + attempt may fail even though there is network connectivity to the Rally server. + For this reason, the use of the ping=True option is discouraged going forward. + The next minor point release of pyral (v1.3.0) will have the default value + for this option inverted to be False. + The the ping operation itself will be dropped in the next major release (2.0.0). + * isolated_workspace (True or False, default in v1.2.0 is False) + Specifies that the Rally instance will only be used for interacting with + a single workspace (either the user's default workspace or the named workspace). + Using isolated_workspace=True provides performance benefits for a subscription + with many workspaces, but it also means you cannot change the workspace you + are working within a single instance of a Rally class, nor can you provide + a workspace keyword argument to a get, create, update or delete methods that + differs from the workspace identified at instantiation time. + For subscriptions with a small to moderate number of workspaces (up to a few dozen), + the performance savings will be relatively minor when using isolated_workspace=True + vs. isolated_workspace=False. However, for subscriptions with a large number of + workspaces, using isolated_workspace=False results in a request to AgileCentral + for each workspace, which can result in a noticeable lag before the instantiation + statement returns a ready-for-use Rally instance. If you use an apikey value, any user name and password you provide is not considered, the connection attempt will only use the apikey. @@ -276,7 +318,7 @@ Rally **pyral**, you must have your account added to the whitelist in Rally so that you can use either BasicAuth (username and password) or the API Key to authenticate to Rally. -.. py:class:: Rally (server, user=None, password=None, apikey=None, workspace=None, project=None, warn=True) +.. py:class:: Rally (server, user=None, password=None, apikey=None, workspace=None, project=None, warn=True, server_ping=True) Examples:: @@ -290,6 +332,8 @@ Examples:: rally = Rally(server, apikey="_some-more-numbers", workspace='RockLobster', project='Fence Posts') + rally = Rally('rally1.rallydev.com', 'chester@corral.com', 'bAbYF@cerZ', server_ping=False) + Core REST methods and CRUD aliases @@ -513,6 +557,9 @@ pyral.Rally instance convenience methods This method offers a convenient one-stop means of obtaining usable information about all users in the named workspace. If no workspace is specified, then the current context's workspace is used. + NOTE: Unless you are using credentials associated with a SubscriptionAdministrator + or WorkspaceAdministrator, you will not be able to access a user's UserProfile + other than yourself. Return a list of User instances (fully hydrated for scalar attributes) whose ref and collection attributes will be lazy eval'ed upon access. @@ -525,6 +572,16 @@ pyral.Rally instance convenience methods PortfolioItem subclass. Intended usage is to use the return *.ref* attribute. For example, within an info dict, "PortfolioItemType" : rally.typedef('Feature').ref . +.. method:: getCollection(collection_url) + + Given a collection_url of the form: + + http(s)://(:)/slm/webservice/v2.0//OID/ + + issue a request for the url and return back a list of hydrated instances + for each item in the collection. + + .. method:: getState(entityName, stateName) As of Rally WSAPI 1.37 (Sep 2012), the State attribute is no longer a String value for @@ -587,6 +644,23 @@ pyral.Rally instance convenience methods Each Attachment record will look like a Rally WSAPI Attachment with the additional Content attribute that will contain the decoded AttachmentContent. +.. method:: rankAbove(reference_artifact, target_artifact) + + Rank the target_artifact above the reference_artifact. + +.. method:: rankBelow(reference_artifact, target_artifact) + + Rank the target_artifact below the reference_artifact. + +.. method:: rankTop(target_artifact) + + Rank the target_artifact at the top of the list of ranked Artifacts + that the target_artifact exists in. + +.. method:: rankBottom(target_artifact) + + Rank the target_artifact at the bottom of the list of ranked Artifacts + that the target_artifact exists in. RallyRESTResponse @@ -649,7 +723,7 @@ Item Attributes the standard dot (.) notation. The names are identical to those documented in the `Rally WS API`_. -.. _Rally WS API: https://rally.rallydev.com/slm/doc/webservice +.. _Rally WS API: https://rally1.rallydev.com/slm/doc/webservice Generally, every concrete instance in the Rally system will have a Name attribute. You can use the **attributes()** method on an instance to obtain the names of all of the @@ -697,7 +771,7 @@ Example:: oid : 12345678 ref : hierarchicalrequirement/12345678 ObjectID : 12345678 - _ref : https://rallydev.rallydev.com/slm/webservice/1.30/hierarchicalrequirement/12345678.js + _ref : https://rallydev.rallydev.com/slm/webservice/v2.0/hierarchicalrequirement/412345678 _CreatedAt : today at 3:14 am _hydrated : True Name : Filbert nuts should be added to all energy bars @@ -715,16 +789,16 @@ Example:: Capitalizable : None Changesets : [] Children : [] - CreationDate : 2012-07-12T09:14:35.852Z + CreationDate : 2016-07-12T09:14:35.852Z DefectStatus : NONE Defects : [] Description : As a health conscious PO, I want better nutritional content in all bars Discussion : [] IdeaURL : IdeaVotes : None - InProgressDate : 2012-07-12T09:14:36.098Z + InProgressDate : 2016-07-12T09:14:36.098Z Iteration : Iteration.ref (OID 1242381 Name Iteration 5 (Summer)) KanbanState : Accepted - LastUpdateDate : 2012-07-12T09:14:36.237Z + LastUpdateDate : 2016-07-12T09:14:36.237Z ... diff --git a/doc/source/overview.rst b/doc/source/overview.rst index 21d6790..116c043 100644 --- a/doc/source/overview.rst +++ b/doc/source/overview.rst @@ -68,7 +68,7 @@ In the Rally vernacular, a logical entity is called a *type*. Some examples of *types* that are usually what a user of **pyral** will be interested in called *artifacts*. An *artifact* is either a UserStory, Defect, Task, DefectSuite, TestCase or TestSet. The Python toolkit for the Rally REST API (**pyral**) is primarily oriented towards operations with artifacts. -But, it is not limited to those as it is very possible view/operate on other Rally +But, it is not limited to those as it is very possible to view/operate on other Rally entities such as Workspace, Project, UserProfile, Release, Iteration, TestCaseResult, TestFolder, Tag and others. diff --git a/examples/add_tcrs.py b/examples/add_tcrs.py index ae0fc56..1cfcc72 100644 --- a/examples/add_tcrs.py +++ b/examples/add_tcrs.py @@ -54,8 +54,8 @@ def main(args): test_case = rally.get('TestCase', query="FormattedID = %s" % test_case_id, workspace=workspace, project=None, instance=True) if not test_case or hasattr(test_case, 'resultCount'): - print "Sorry, unable to find a TestCase with a FormattedID of %s in the %s workspace" % \ - (test_case_id, workspace) + print("Sorry, unable to find a TestCase with a FormattedID of %s in the %s workspace" % \ + (test_case_id, workspace)) sys.exit(3) wksp = rally.getWorkspace() @@ -69,12 +69,12 @@ def main(args): } try: tcr = rally.create('TestCaseResult', tcr_data) - except RallyRESTAPIError, details: + except RallyRESTAPIError as details: sys.stderr.write('ERROR: %s \n' % details) sys.exit(4) - print "Created TestCaseResult OID: %s TestCase: %s Build: %s Date: %s Verdict: %s" % \ - (tcr.oid, test_case.FormattedID, tcr.Build, tcr.Date, tcr.Verdict) + print("Created TestCaseResult OID: %s TestCase: %s Build: %s Date: %s Verdict: %s" % \ + (tcr.oid, test_case.FormattedID, tcr.Build, tcr.Date, tcr.Verdict)) ################################################################################################# ################################################################################################# diff --git a/examples/builddefs.py b/examples/builddefs.py index b06d0d7..94e8e20 100644 --- a/examples/builddefs.py +++ b/examples/builddefs.py @@ -43,38 +43,38 @@ def main(args): options = [opt for opt in args if opt.startswith('--')] - server, user, password, apikey, workspace, project = rallyWorkset(options) + server, username, password, apikey, workspace, project = rallyWorkset(options) if apikey: rally = Rally(server, apikey=apikey, workspace=workspace) else: - rally = Rally(server, user=user, password=password, workspace=workspace) + rally = Rally(server, user=username, password=password, workspace=workspace) rally.enableLogging("rally.history.blddefs") for workspace, project in wps: rally.setWorkspace(workspace) - print "workspace: %s project: %s\n" % (workspace, project) + print("workspace: %s project: %s\n" % (workspace, project)) response = rally.get('BuildDefinition', fetch=True, query='Project.Name = "%s"' % project, order='Name', workspace=workspace, project=project) if response.errors: - print response.errors + print(response.errors) sys.exit(9) - print "%-12.12s %-10.10s %-36.36s %12s %-20.20s %s" % \ - ('BuildDef OID', 'CreateDate', 'BuildDefinition.Name', 'LastStatus', 'LastBuildDateTime', 'NumBuilds') - print "%-12.12s %-10.10s %-36.36s %10s %-19.19s %s" % \ - ('-' * 12, '-' * 10, '-' * 36, '-' * 10, '-' * 19, '-' * 9) + print("%-12.12s %-10.10s %-36.36s %12s %-20.20s %s" % \ + ('BuildDef OID', 'CreateDate', 'BuildDefinition.Name', 'LastStatus', 'LastBuildDateTime', 'NumBuilds')) + print("%-12.12s %-10.10s %-36.36s %10s %-19.19s %s" % \ + ('-' * 12, '-' * 10, '-' * 36, '-' * 10, '-' * 19, '-' * 9)) for builddef in response: if builddef.LastStatus == "NO BUILDS": - print "%s %s %-24.24s NO BUILDS" % \ - (builddef.oid, builddef.CreationDate[:10], builddef.Name) + print("%s %s %-24.24s NO BUILDS" % \ + (builddef.oid, builddef.CreationDate[:10], builddef.Name)) continue lbt = builddef.LastBuild.CreationDate.split('T') last_build_time = "%s %s" % (lbt[0], lbt[1][:8] ) bdf = "%12.12s %-10.10s %-36.36s %12s %-20.20s %4s" - print bdf % (builddef.oid, builddef.CreationDate[:10], + print(bdf % (builddef.oid, builddef.CreationDate[:10], builddef.Name, builddef.LastStatus, last_build_time, - len(builddef.Builds)) + len(builddef.Builds))) ################################################################################################## ################################################################################################## diff --git a/examples/creattach.py b/examples/creattach.py index cdf5620..2cec1af 100644 --- a/examples/creattach.py +++ b/examples/creattach.py @@ -68,11 +68,11 @@ def main(args): artifact = validateTarget(rally, target) - me = rally.getUserInfo(username=user).pop(0) - #print "%s user oid: %s" % (user, me.oid) + me = rally.getUserInfo(username=username).pop(0) + #print "%s user oid: %s" % (username, me.oid) att = rally.addAttachment(artifact, attachment_file_name) - print "created Attachment: %s for %s" % (attachment_file_name, target) + print("created Attachment: %s for %s" % (attachment_file_name, target)) ################################################################################################# diff --git a/examples/crtask.py b/examples/crtask.py index 56a56bf..eca1b87 100644 --- a/examples/crtask.py +++ b/examples/crtask.py @@ -55,9 +55,9 @@ def main(args): "Notes" : "I have really only done some daydreaming wrt this task. Sorry Jane, I knew you had big plans for Frankie's blowout BBQ next month, but the honeycomb harvest project is taking all my time." } - print "Creating Task ..." + print("Creating Task ...") task = rally.put('Task', info) - print "Created Task: %s OID: %s" % (task.FormattedID, task.oid) + print("Created Task: %s OID: %s" % (task.FormattedID, task.oid)) ################################################################################################# @@ -91,9 +91,9 @@ def queryForTasks(rally): for task in response: #print "%s %s %s %s" % (task.__class__.__name__, task.oid, task.name, task._ref) - print "%s %s %s %s %s %s" % (task.FormattedID, task.Name, + print("%s %s %s %s %s %s" % (task.FormattedID, task.Name, task.Workspace.Name, task.Project.Name, - task.Release.Name, task.Iteration.Name) + task.Release.Name, task.Iteration.Name)) ################################################################################################# ################################################################################################# diff --git a/examples/defrevs.py b/examples/defrevs.py index 533190c..ab37c54 100644 --- a/examples/defrevs.py +++ b/examples/defrevs.py @@ -20,8 +20,8 @@ def main(args): options = [opt for opt in args if opt.startswith('--')] args = [arg for arg in args if arg not in options] - server, user, password, apikey, workspace, project = rallyWorkset(options) - rally = Rally(server, user, password, apikey=apikey, workspace=workspace, project=project) + server, username, password, apikey, workspace, project = rallyWorkset(options) + rally = Rally(server, username, password, apikey=apikey, workspace=workspace, project=project) target = args.pop(0) fields = "FormattedID,State,Name,CreationDate,RevisionHistory,Revisions" @@ -29,13 +29,13 @@ def main(args): defect = rally.get('Defect', fetch=fields, query=criteria, instance=True) - print "%s %10.10s %-11s %s" % (defect.FormattedID, defect.CreationDate, - defect.State, defect.Name) - print "" + print("%s %10.10s %-11s %s" % (defect.FormattedID, defect.CreationDate, + defect.State, defect.Name)) + print("") for rev in reversed(defect.RevisionHistory.Revisions): - print "%d) %-22.22s %-16.16s %s\n" % \ + print("%d) %-22.22s %-16.16s %s\n" % \ (rev.RevisionNumber, rev.CreationDate.replace('T', ' '), - rev.User.DisplayName, rev.Description) + rev.User.DisplayName, rev.Description)) ################################################################################################# ################################################################################################# diff --git a/examples/get_attachments.py b/examples/get_attachments.py index 6a0a861..5360acc 100644 --- a/examples/get_attachments.py +++ b/examples/get_attachments.py @@ -33,7 +33,8 @@ def main(args): options = [opt for opt in args if opt.startswith('--')] args = [arg for arg in args if arg not in options] - server, user, password, apikey, workspace, project = rallyWorkset(options) + server, username, password, apikey, workspace, project = rallyWorkset(options) + print " | ".join([server, username, password, workspace, project]) if apikey: rally = Rally(server, apikey=apikey, workspace=workspace, project=project) else: @@ -75,12 +76,12 @@ def main(args): artifact = response.next() attachments = rally.getAttachments(artifact) for attachment in attachments: - print "-" * 32 - print attachment.Name - print "~" * len(attachment.Name) - print attachment.Content - print "" - print "=" * 64 + print("-" * 32) + print(attachment.Name) + print("~" * len(attachment.Name)) + print(attachment.Content) + print("") + print("=" * 64) ################################################################################################# ################################################################################################# diff --git a/examples/get_schedulable_artifacts.py b/examples/get_schedulable_artifacts.py index 71f4738..1bf7d94 100644 --- a/examples/get_schedulable_artifacts.py +++ b/examples/get_schedulable_artifacts.py @@ -27,25 +27,25 @@ def main(args): options = [opt for opt in args if opt.startswith('--')] args = [arg for arg in args if arg not in options] - server, user, password, apikey, workspace, project = rallyWorkset(options) - rally = Rally(server, user, password, apikey=apikey, workspace=workspace) + server, username, password, apikey, workspace, project = rallyWorkset(options) + rally = Rally(server, username, password, apikey=apikey, workspace=workspace) wksp = rally.getWorkspace() - print "Workspace: %s" % wksp.Name - print "=" * (len(wksp.Name) + 12) - print "" + print("Workspace: %s" % wksp.Name) + print("=" * (len(wksp.Name) + 12)) + print("") for project in PROJECTS: rally.setProject(project) - print " %s" % project - print " %s" % ('-' * len(project)) - print "" + print(" %s" % project) + print(" %s" % ('-' * len(project))) + print("") response = getSchedulableArtifacts(rally) showSchedulableArtifacts(response) - print "" - print "-" * 80 - print "" - print "%d items" % response.resultCount - print "" + print("") + print("-" * 80) + print("") + print("%d items" % response.resultCount) + print("") ################################################################################################# @@ -75,8 +75,8 @@ def showSchedulableArtifacts(items): release = sched_art.Release.Name if sched_art.Release else "" iteration = sched_art.Iteration.Name if sched_art.Iteration else "" accepted = sched_art.AcceptedDate if hasattr(sched_art, 'AcceptedDate') else "" - print " %-7.7s %-64.64s %-12.12s %16.16s %s" % \ - (sched_art.FormattedID, sched_art.Name, release, iteration, accepted) + print(" %-7.7s %-64.64s %-12.12s %16.16s %s" % \ + (sched_art.FormattedID, sched_art.Name, release, iteration, accepted)) ################################################################################################# ################################################################################################# diff --git a/examples/get_schema.py b/examples/get_schema.py index 994c54a..6a11d2f 100644 --- a/examples/get_schema.py +++ b/examples/get_schema.py @@ -67,11 +67,15 @@ def main(args): errout("ERROR: You must supply an entity name!\n") sys.exit(1) - server, user, password, apikey, workspace, project = rallyWorkset(options) - if apikey: - rally = Rally(server, apikey=apikey, workspace=workspace, project=project) - else: - rally = Rally(server, user=username, password=password, workspace=workspace, project=project) + server, username, password, apikey, workspace, project = rallyWorkset(options) + try: + if apikey: + rally = Rally(server, apikey=apikey, workspace=workspace, project=project) + else: + rally = Rally(server, user=username, password=password, workspace=workspace, project=project) + except Exception as ex: + errout(str(ex.args[0])) + sys.exit(1) entity = args[0] if entity in ['UserStory', 'User Story', 'Story']: @@ -79,14 +83,8 @@ def main(args): #if '/' in entity: # parent, entity = entity.split('/', 1) - try: - rally = Rally(server, user=user, password=password) - except Exception as ex: - errout(str(ex.args[0])) - sys.exit(1) - schema_item = rally.typedef(entity) - print schema_item + print(schema_item) ################################################################################################# ################################################################################################# diff --git a/examples/getitem.py b/examples/getitem.py index 504fe65..22b229b 100644 --- a/examples/getitem.py +++ b/examples/getitem.py @@ -45,7 +45,6 @@ def main(args): else: rally = Rally(server, user=username, password=password, workspace=workspace, project=project) - rally = Rally(server, user, password) # specify the Rally server and credentials rally.enableLogging('rally.hist.item') # name of file you want logging to go to if len(args) != 2: @@ -82,7 +81,7 @@ def main(args): sys.exit(5) for item in response: - print item.details() + print(item.details()) ################################################################################################# ################################################################################################# diff --git a/examples/periscope.py b/examples/periscope.py index 4963dd4..a21be06 100644 --- a/examples/periscope.py +++ b/examples/periscope.py @@ -23,10 +23,10 @@ def main(args): rally.enableLogging('rally.hist.periscope') # name of file for logging content for workspace in rally.getWorkspaces(): - print "%s %s" % (workspace.oid, workspace.Name) + print("%s %s" % (workspace.oid, workspace.Name)) for project in rally.getProjects(workspace=workspace.Name): - print " %12.12s %-36.36s |%s|" % (project.oid, project.Name, project.State) - print "" + print(" %12.12s %-36.36s |%s|" % (project.oid, project.Name, project.State)) + print("") ################################################################################################# ################################################################################################# diff --git a/examples/repoitems.py b/examples/repoitems.py index 6ad0b06..74f585e 100644 --- a/examples/repoitems.py +++ b/examples/repoitems.py @@ -48,7 +48,7 @@ def main(args): options = [opt for opt in args if opt.startswith('--')] args = [arg for arg in args if arg not in options] if not args: - print USAGE + print(USAGE) sys.exit(9) server, username, password, apikey, workspace, project = rallyWorkset(options) if apikey: @@ -87,8 +87,8 @@ def showRepoItems(rally, repo_name, workspace=None, limit=200, order="ASC", sinc query=criteria, workspace=workspace, project=None, pagesize=200, limit=limit) - except Exception, msg: - print msg + except Exception as msg: + print(msg) return None if response.errors: @@ -96,15 +96,27 @@ def showRepoItems(rally, repo_name, workspace=None, limit=200, order="ASC", sinc response.status_code) return None - print "Workspace: %s SCMRepository: %s Changesets: %s " % \ - (workspace, repo_name, response.resultCount) + print("Workspace: %s SCMRepository: %s Changesets: %s " % \ + (workspace, repo_name, response.resultCount)) for cs in response: author = cs.Author.UserName if cs.Author else "-None-" committed = cs.CommitTimestamp.replace('T', ' ') - print "%-12.12s %-42.42s %-19.19s Z %s %s" % \ - (cs.SCMRepository.Name, cs.Revision, committed, author, cs.oid) - print " |%s|" % cs.Message + print("%-12.12s %-42.42s %-19.19s Z %s %s" % \ + (cs.SCMRepository.Name, cs.Revision, committed, author, cs.oid)) + print(" |%s|" % cs.Message) + + if len(cs.Artifacts) == 0: + print " changeset %s - %s has no artifacts" % (cs.SCMRepository.Name, cs.Revision) + continue + + artifact_idents = [art.FormattedID for art in cs.Artifacts] + if artifact_idents: + print(" artifacts mentioned: %s" % " ".join(artifact_idents)) +## +## for artifact in cs.Artifacts: +## print(" %s |%s| |%s|" % (artifact.FormattedID, artifact.Workspace.Name, artifact.Project.Name)) +## # If we iterate over change items via cs.Changes, then we later have to do lazy load # for the change attributes on a per Change basis, which is relatively slow @@ -117,25 +129,6 @@ def showRepoItems(rally, repo_name, workspace=None, limit=200, order="ASC", sinc # for change in changes: # print " %s %s" % (change.Action, change.PathAndFilename) - if len(cs.Artifacts) == 0: - #print "changeset %s - %s has no artifacts" % (cs.SCMRepository.Name, cs.Revision) - continue - - artifact_idents = [] - for shell_artifact in cs.Artifacts: - entity = shell_artifact._type # the shell artifact has the oid, not the FormattedID - if shell_artifact.oid not in oid_cache: - by_oid = "ObjectID = %s" % shell_artifact.oid - art_response = rally.get(entity, fetch="FormattedID", query=by_oid) - if art_response.resultCount == 1: - art = art_response.next() - oid_cache[shell_artifact.oid] = art - artifact = oid_cache.get(shell_artifact.oid, None) - if artifact: - artifact_idents.append(artifact.FormattedID) - if artifact_idents: - print " artifacts mentioned: %s" % " ".join(artifact_idents) - ################################################################################################# ################################################################################################# diff --git a/examples/showdefects.py b/examples/showdefects.py index 7483563..29e41ce 100644 --- a/examples/showdefects.py +++ b/examples/showdefects.py @@ -32,10 +32,10 @@ def main(args): pagesize=200, limit=400) for defect in response: - print "%-8.8s %-52.52s %s" % (defect.FormattedID, defect.Name, defect.State) + print("%-8.8s %-52.52s %s" % (defect.FormattedID, defect.Name, defect.State)) - print "-----------------------------------------------------------------" - print response.resultCount, "qualifying defects" + print("-----------------------------------------------------------------") + print(response.resultCount, "qualifying defects") ################################################################################################# ################################################################################################# diff --git a/examples/statecounts.py b/examples/statecounts.py index a882151..a99885a 100644 --- a/examples/statecounts.py +++ b/examples/statecounts.py @@ -76,12 +76,12 @@ def show_counts(rally, artifact_type, state, state_values, av_time): response = rally.get(artifact_type, fetch="FormattedID", query='%s = %s' % (state, state_value), projectScopeUp=False, projectScopeDown=False) if response.errors: - print "ERROR detected %s" % response.errors[0] + print("ERROR detected %s" % response.errors[0]) sys.exit(1) output.append("%16s : %5d" % (state_value, response.resultCount)) for line in output: - print line + print(line) ################################################################################################### ################################################################################################### diff --git a/examples/typedef.py b/examples/typedef.py index 67268ed..0d67cc9 100644 --- a/examples/typedef.py +++ b/examples/typedef.py @@ -59,7 +59,7 @@ def main(args): options = [opt for opt in args if opt.startswith('--')] args = [arg for arg in args if arg not in options] if not args: - print "You must supply an entity name!" + print("You must supply an entity name!") sys.exit(1) query = "" @@ -84,11 +84,11 @@ def main(args): typedef = rally.typedef(target) showAttributes(typedef.Attributes) - print "" - print "-" * 64 - print "" + print("") + print("-" * 64) + print("") for ix, ancestor in enumerate(typedef.inheritanceChain()): - print "%s %s" % (" " * (ix*4), ancestor) + print("%s %s" % (" " * (ix*4), ancestor)) ################################################################################################# @@ -129,7 +129,7 @@ def showAttributes(attributes): tank.append(" ... %d more values not shown" % (len(allowedValues) - av_limit)) for item in required + optional: - print item.encode('utf-8') + print(item.encode('utf-8')) ################################################################################################# ################################################################################################# diff --git a/examples/updtag.py b/examples/updtag.py new file mode 100644 index 0000000..ecfea4c --- /dev/null +++ b/examples/updtag.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +################################################################################################# +# +# updtag.py - example of how to effect an update to an existing tag +# this script changes the name for a specific Tag +# +USAGE = """\ +Usage: python updtag.py +""" +################################################################################################# + +import sys, os + +from pyral import Rally, rallyWorkset, RallyRESTAPIError + +################################################################################################# + +errout = sys.stderr.write + +################################################################################################# + +def main(args): + options = [opt for opt in args if opt.startswith('--')] + args = [arg for arg in args if arg not in options] + server, user, password, apikey, workspace, project = rallyWorkset(options) + rally = Rally(server, user, password, apikey=apikey, workspace=workspace) + rally.enableLogging("rally.history.updtag") + + if len(args) != 2: + print(USAGE) + sys.exit(1) + + target_name, new_name = args[:2] + target_oid = None + + response = rally.get('Tag', fetch="true", order="Name", server_ping=False, isolated_workspace=True) + for tag in response: + print("Workspace %s has tag: %-14.14s created on %s Name: %s" % \ + (tag.Workspace.Name, tag.oid, tag.CreationDate[:-5].replace('T', ' '), tag.Name)) + if tag.Name == target_name: + target_oid = tag.oid + + if not target_oid: + print("No Tag exists with a Name value of |%s|" % target_name) + sys.exit(1) + + info = {"ObjectID" : target_oid, "Name" : new_name } + print(info) + + print("attempting to update Tag with Name of '%s' to '%s' ..." % (target_name, new_name)) + try: + tag = rally.update('Tag', info) + except RallyRESTAPIError, details: + sys.stderr.write('ERROR: %s \n' % details) + sys.exit(2) + + print("Tag updated") + print("ObjectID: %s Name: %s " % (tag.oid, tag.Name)) + +################################################################################################# +################################################################################################# + +if __name__ == '__main__': + main(sys.argv[1:]) + sys.exit(0) + diff --git a/examples/uptask.py b/examples/uptask.py index 2f44741..bb066eb 100644 --- a/examples/uptask.py +++ b/examples/uptask.py @@ -34,7 +34,7 @@ def main(args): rally.enableLogging("rally.history.uptask") taskID = args.pop() # for this example use the FormattedID - print "attempting to update Task: %s" % taskID + print("attempting to update Task: %s" % taskID) # # following assumes there is: @@ -80,12 +80,12 @@ def main(args): try: task = rally.update('Task', info) - except RallyRESTAPIError, details: + except RallyRESTAPIError as details: sys.stderr.write('ERROR: %s \n' % details) sys.exit(2) - print "Task updated" - print "ObjectID: %s FormattedID: %s" % (task.oid, task.FormattedID) + print("Task updated") + print("ObjectID: %s FormattedID: %s" % (task.oid, task.FormattedID)) ################################################################################################# ################################################################################################# diff --git a/examples/wkspcounts.py b/examples/wkspcounts.py index 75617a0..80d1716 100644 --- a/examples/wkspcounts.py +++ b/examples/wkspcounts.py @@ -49,21 +49,21 @@ def main(args): workspaces = hits for wksp in workspaces: - print wksp.Name - print "=" * len(wksp.Name) + print(wksp.Name) + print("=" * len(wksp.Name)) rally.setWorkspace(wksp.Name) projects = [None] if byproject: projects = rally.getProjects(workspace=wksp.Name) for project in projects: if project: - print "" - print " %s" % project.Name - print " %s" % ('-' * len(project.Name)) + print("") + print(" %s" % project.Name) + print(" %s" % ('-' * len(project.Name))) for artifact_type in art_types: count = getArtifactCount(rally, artifact_type, project=project) - print " %-16.16s : %4d items" % (artifact_type, count) - print "" + print(" %-16.16s : %4d items" % (artifact_type, count)) + print("") ################################################################################################### @@ -112,7 +112,7 @@ def getArtifactCount(rally, artifact_type, project=None): project=None, projectScopeUp=False, projectScopeDown=False) if response.errors: - print "Blarrggghhh! %s query error %s" % (artifact_type, response.errors[0]) + print("Blarrggghhh! %s query error %s" % (artifact_type, response.errors[0])) return 0 return response.resultCount diff --git a/pyral/__init__.py b/pyral/__init__.py index e31adea..32cdd25 100644 --- a/pyral/__init__.py +++ b/pyral/__init__.py @@ -1,4 +1,4 @@ -__version__ = (1, 1, 1) +__version__ = (1, 2, 0) from .config import rallySettings, rallyWorkset from .restapi import Rally, RallyRESTAPIError, RallyUrlBuilder from .rallyresp import RallyRESTResponse diff --git a/pyral/config.py b/pyral/config.py index e95ad50..0c26ada 100644 --- a/pyral/config.py +++ b/pyral/config.py @@ -1,4 +1,4 @@ -#!/usr/local/bin/python2.7 +#!/usr/local/bin/python3.5 ################################################################################################### # @@ -6,7 +6,7 @@ # ################################################################################################### -__version__ = (1, 1, 1) +__version__ = (1, 2, 0) import datetime import os @@ -35,12 +35,12 @@ { #'X-RallyIntegrationName' : 'Python toolkit for Rally REST API', # although syntactically this is the more correct 'X-RallyIntegrationName' : 'Rally REST API toolkit for Python', # this matches the format of the other language toolkits - 'X-RallyIntegrationVendor' : 'Rally Software Development', + 'X-RallyIntegrationVendor' : 'CA Technologies', 'X-RallyIntegrationVersion' : '%s.%s.%s' % __version__, 'X-RallyIntegrationLibrary' : 'pyral-%s.%s.%s' % __version__, 'X-RallyIntegrationPlatform' : 'Python %s' % platform.python_version(), 'X-RallyIntegrationOS' : platform.platform(), - 'User-Agent' : 'Pyral Rally WebServices Agent', + 'User-Agent' : 'Pyral Agile Central WebServices Agent', 'Content-Type' : 'application/json', 'Accept-Encoding' : 'gzip' } @@ -58,26 +58,31 @@ def timestamp(): RALLY_ARG_SETTING_PATT2 = re.compile('^--([ASUPWasupw][a-z]+)=(.+)\s*$') RALLY_CONFIG_FILE_PATT = re.compile('^--(cfg|conf|config|rallyConfig)=(\S+)$') +TRUTHY_VALUES = ['t', 'true', 'y', 'yes', '1'] +FALSEY_VALUES = ['f', 'false', 'n', 'no', '0'] + ################################################################################ -def rallySettings(args): +def rallyWorkset(args): """ + intended to supplant rallySettings as of pyral 2.0.x + priority order of Python Rally REST API server ident, credentials, workspace/project: - 1) command line args with --rallyServer, --rallyUser, --rallyPassword, --workspace, --project + 1) command line args with --rallyServer, --rallyUser, --rallyPassword, --apikey, --workspace, --project, --ping 2) command line arg specifying a config file --rallyConfig= or --config= or --conf= or --cfg= 3) ENV variable with location of rally-.cfg --> RALLY_CONFIG 4) current directory with rally-.cfg - 5) RALLY_SERVER, RALLY_USER_NAME, RALLY_PASSWORD, RALLY_WORKSPACE, RALLY_PROJECT env VARS + 5) RALLY_SERVER, RALLY_USER_NAME, RALLY_PASSWORD, APIKEY, RALLY_WORKSPACE, RALLY_PROJECT, RALLY_PING env VARS 6) SERVER, USER_NAME, PASSWORD defined in this module start by priming the return values with #6 and work your way up the priority ladder """ # #6 # start with the defaults defined in this module - server_creds = [SERVER, USER_NAME, PASSWORD, "default", "default"] + server_creds = [SERVER, USER_NAME, PASSWORD, "", "default", "default"] def snarfSettings(targetFile, server_creds): """ @@ -103,12 +108,21 @@ def snarfSettings(targetFile, server_creds): server_creds[1] = value elif item == 'PASSWORD': server_creds[2] = value - elif item == 'WORKSPACE': + elif item == "APIKEY": server_creds[3] = value - elif item == 'PROJECT': + elif item == 'WORKSPACE': server_creds[4] = value + elif item == 'PROJECT': + server_creds[5] = value + elif item == 'RALLY_PING': + if value.lower() in TRUTHY_VALUES: + os.environ['RALLY_PING'] = 'true' + elif value.lower() in FALSEY_VALUES: + os.environ['RALLY_PING'] = 'false' + else: + os.environ['RALLY_PING'] = 'true' cf.close() - sc = "%s, %s, %s, %s, %s" % tuple(server_creds) + sc = "%s, %s, %s, %s, %s, %s" % tuple(server_creds) return server_creds except Exception as ex: pass @@ -116,10 +130,11 @@ def snarfSettings(targetFile, server_creds): # #5 # if there are environment vars, use them # - for ix, name in enumerate(['RALLY_SERVER', 'RALLY_USER', 'RALLY_PASSWORD', 'RALLY_WORKSPACE', 'RALLY_PROJECT']): + # purposely excluding RALLY_PING ... + for ix, name in enumerate(['RALLY_SERVER', 'RALLY_USER', 'RALLY_PASSWORD', 'APIKEY', 'RALLY_WORKSPACE', 'RALLY_PROJECT']): if name in os.environ: server_creds[ix] = os.environ[name] - + # #4 # if there is a rally-.cfg file in the current directory matching the WS_API_VERSION # load with contents of that file @@ -129,8 +144,8 @@ def snarfSettings(targetFile, server_creds): if target_version_config in entries: server_creds = snarfSettings(target_version_config, server_creds) else: - print "Ignoring non-matching version of Rally config settings: %s (working version: %s)" % \ - (entries.pop(), WS_API_VERSION) + print("Ignoring non-matching version of Rally config settings: %s (working version: %s)" % \ + (entries.pop(), WS_API_VERSION)) # #3 # if there is a RALLY_CONFIG environment variable pointing to a file, load with contents of file @@ -151,7 +166,7 @@ def snarfSettings(targetFile, server_creds): # #1 # now look at the args (from command line invocation) # grab any --rallyServer=?, --rallyUser=?, --rallyPassword=?, --rallyWorkspace=?, --rallyProject=? in args - # grab any --server=?, --user=?, --password=?, --workspace=?, --project=? in args + # grab any --server=?, --user=?, --password=?, --apikey=?, --workspace=?, --project=? --ping=?in args for arg in args: mo = RALLY_ARG_SETTING_PATT1.match(arg) if mo: @@ -162,10 +177,12 @@ def snarfSettings(targetFile, server_creds): server_creds[1] = value elif item == 'rallyPassword': server_creds[2] = value + #elif item = 'rallyApikey': # enable this if we ever decide that apikey arg should ever be specified as --rallyApikey + # server_creds[3] = value elif item == 'rallyWorkspace': - server_creds[3] = value - elif item == 'rallyProject': server_creds[4] = value + elif item == 'rallyProject': + server_creds[5] = value mo = RALLY_ARG_SETTING_PATT2.match(arg) if mo: @@ -176,35 +193,42 @@ def snarfSettings(targetFile, server_creds): server_creds[1] = value elif item == 'password': server_creds[2] = value - elif item == 'workspace': + elif item == 'apikey': server_creds[3] = value - elif item == 'project': + elif item == 'workspace': server_creds[4] = value + elif item == 'project': + server_creds[5] = value + elif item == 'ping': + if value.lower() in TRUTHY_VALUES: + os.environ['RALLY_PING'] = 'true' + elif value.lower() in FALSEY_VALUES: + os.environ['RALLY_PING'] = 'false' + else: + os.environ['RALLY_PING'] = 'true' return server_creds -################################################################################################### +################################################################################ -def rallyWorkset(args): +def rallySettings(args): """ - intended to supplant rallySettings as of pyral 2.0.x - priority order of Python Rally REST API server ident, credentials, workspace/project: - 1) command line args with --rallyServer, --rallyUser, --rallyPassword, --apikey, --workspace, --project + 1) command line args with --rallyServer, --rallyUser, --rallyPassword, --workspace, --project 2) command line arg specifying a config file --rallyConfig= or --config= or --conf= or --cfg= 3) ENV variable with location of rally-.cfg --> RALLY_CONFIG 4) current directory with rally-.cfg - 5) RALLY_SERVER, RALLY_USER_NAME, RALLY_PASSWORD, APIKEY, RALLY_WORKSPACE, RALLY_PROJECT env VARS + 5) RALLY_SERVER, RALLY_USER_NAME, RALLY_PASSWORD, RALLY_WORKSPACE, RALLY_PROJECT env VARS 6) SERVER, USER_NAME, PASSWORD defined in this module start by priming the return values with #6 and work your way up the priority ladder """ # #6 # start with the defaults defined in this module - server_creds = [SERVER, USER_NAME, PASSWORD, "", "default", "default"] + server_creds = [SERVER, USER_NAME, PASSWORD, "default", "default"] def snarfSettings(targetFile, server_creds): """ @@ -230,14 +254,12 @@ def snarfSettings(targetFile, server_creds): server_creds[1] = value elif item == 'PASSWORD': server_creds[2] = value - elif item == "APIKEY": - server_creds[3] = value elif item == 'WORKSPACE': - server_creds[4] = value + server_creds[3] = value elif item == 'PROJECT': - server_creds[5] = value + server_creds[4] = value cf.close() - sc = "%s, %s, %s, %s, %s, %s" % tuple(server_creds) + sc = "%s, %s, %s, %s, %s" % tuple(server_creds) return server_creds except Exception as ex: pass @@ -245,7 +267,7 @@ def snarfSettings(targetFile, server_creds): # #5 # if there are environment vars, use them # - for ix, name in enumerate(['RALLY_SERVER', 'RALLY_USER', 'RALLY_PASSWORD', 'APIKEY', 'RALLY_WORKSPACE', 'RALLY_PROJECT']): + for ix, name in enumerate(['RALLY_SERVER', 'RALLY_USER', 'RALLY_PASSWORD', 'RALLY_WORKSPACE', 'RALLY_PROJECT']): if name in os.environ: server_creds[ix] = os.environ[name] @@ -258,8 +280,8 @@ def snarfSettings(targetFile, server_creds): if target_version_config in entries: server_creds = snarfSettings(target_version_config, server_creds) else: - print "Ignoring non-matching version of Rally config settings: %s (working version: %s)" % \ - (entries.pop(), WS_API_VERSION) + print("Ignoring non-matching version of Rally config settings: %s (working version: %s)" % \ + (entries.pop(), WS_API_VERSION)) # #3 # if there is a RALLY_CONFIG environment variable pointing to a file, load with contents of file @@ -280,7 +302,7 @@ def snarfSettings(targetFile, server_creds): # #1 # now look at the args (from command line invocation) # grab any --rallyServer=?, --rallyUser=?, --rallyPassword=?, --rallyWorkspace=?, --rallyProject=? in args - # grab any --server=?, --user=?, --password=?, --apikey=?, --workspace=?, --project=? in args + # grab any --server=?, --user=?, --password=?, --workspace=?, --project=? in args for arg in args: mo = RALLY_ARG_SETTING_PATT1.match(arg) if mo: @@ -291,12 +313,10 @@ def snarfSettings(targetFile, server_creds): server_creds[1] = value elif item == 'rallyPassword': server_creds[2] = value - #elif item = 'rallyApikey': # enable this if we ever decide that apikey arg should ever be specified as --rallyApikey - # server_creds[3] = value elif item == 'rallyWorkspace': - server_creds[4] = value + server_creds[3] = value elif item == 'rallyProject': - server_creds[5] = value + server_creds[4] = value mo = RALLY_ARG_SETTING_PATT2.match(arg) if mo: @@ -307,11 +327,12 @@ def snarfSettings(targetFile, server_creds): server_creds[1] = value elif item == 'password': server_creds[2] = value - elif item == 'apikey': - server_creds[3] = value elif item == 'workspace': - server_creds[4] = value + server_creds[3] = value elif item == 'project': - server_creds[5] = value + server_creds[4] = value return server_creds + +################################################################################################### +################################################################################################### diff --git a/pyral/context.py b/pyral/context.py index b7307a9..da1cbf3 100644 --- a/pyral/context.py +++ b/pyral/context.py @@ -1,4 +1,4 @@ -#!/usr/local/bin/python2.7 +#!/usr/local/bin/python3.5 ################################################################################################### # @@ -8,7 +8,7 @@ # ################################################################################################### -__version__ = (1, 1, 1) +__version__ = (1, 2, 0) import sys, os import platform @@ -90,11 +90,12 @@ def __repr__(self): class RallyContextHelper(object): - def __init__(self, agent, server, user, password): + def __init__(self, agent, server, user, password, server_ping): self.agent = agent self.server = server self.user = user self.password = password + self.server_ping = server_ping # capture this user's User, UserProfile, Subscription records to extract # the workspaces and projects this user has access to (and their defaults) @@ -111,11 +112,12 @@ def __init__(self, agent, server, user, password): self._project_ref = {} # key by workspace name with dict of project_name: project_ref self._defaultProject = None self._currentProject = None - self.context = RallyContext(server, user, password, self.agent.serviceURL()) - self.defaultContext = self.context # to be updated on check call + self.context = RallyContext(server, user, password, self.agent.serviceURL()) + self.defaultContext = self.context # to be updated on check call + self.operatingContext = self.context # to be updated on check call - def check(self, server): + def check(self, server, workspace, project, isolated_workspace): """ Make an initial attempt to contact the Rally web server and retrieve info for the user associated with the credentials supplied upon instantiation. @@ -127,11 +129,12 @@ def check(self, server): the user. """ ## -## print " RallyContextHelper.check starting ..." +## print(" RallyContextHelper.check starting ...") ## sys.stdout.flush() ## socket.setdefaulttimeout(INITIAL_REQUEST_TIME_LIMIT) target_host = server + self.isolated_workspace = isolated_workspace big_proxy = os.environ.get('HTTPS_PROXY', False) small_proxy = os.environ.get('https_proxy', False) @@ -140,91 +143,136 @@ def check(self, server): if proxy: if proxy.startswith('http'): proxy = SCHEME_PREFIX_PATT.sub('', proxy) - proxy_host, proxy_port = proxy.split(':') + creds, proxy_port, proxy_host = "", "", proxy + if proxy.count('@') == 1: + creds, proxy = proxy.split('@') + proxy_host = proxy + if proxy.count(":") == 1: + proxy_host, proxy_port = proxy.split(':') +## +## print("your proxy host is set to: |%s|" % (proxy_host)) +## target_host = proxy_host or server - reachable, problem = Pinger.ping(target_host) - if not reachable: - if not problem: - problem = "host: '%s' non-existent or unreachable" % target_host - raise RallyRESTAPIError(problem) + if self.server_ping: + reachable, problem = Pinger.ping(target_host) + if not reachable: + if not problem: + problem = "host: '%s' non-existent or unreachable" % target_host + raise RallyRESTAPIError(problem) + + user_response = self._getUserInfo() + subscription = self._loadSubscription() + # caller must either specify a valid workspace/project + # or must have a DefaultWorkspace/DefaultProject in their UserProfile + self._getDefaults(user_response) + + if workspace: + workspaces = self._getSubscriptionWorkspaces(subscription, workspace=workspace, limit=10) + if not workspaces: + problem = "Specified workspace of '%s' either does not exist or the user does not have permission to access that workspace" + raise RallyRESTAPIError(problem % workspace) + if len(workspaces) > 1: + problem = "Multiple workspaces (%d) found with the same name of '%s'. " +\ + "You must specify a workspace with a unique name." + raise RallyRESTAPIError(problem % (len(workspaces), workspace)) + self._currentWorkspace = workspaces[0].Name + + if not workspace and not self._defaultWorkspace: + problem = "No Workspace was specified and there is no DefaultWorkspace setting for the user" + raise RallyRESTAPIError(problem) + + if not workspace and self._defaultWorkspace: + workspaces = self._getSubscriptionWorkspaces(subscription, workspace=self._defaultWorkspace, limit=10) + + if not self.isolated_workspace: + self._getSubscriptionWorkspaces(subscription, limit=0) + self._getWorkspacesAndProjects(workspace=self._currentWorkspace, project=self._defaultProject) + self._setOperatingContext(project) + schema_info = self.agent.getSchemaInfo(self._currentWorkspace) + processSchemaInfo(self.getWorkspace(), schema_info) + + def _getUserInfo(self): # note the use of the _disableAugments keyword arg in the call user_name_query = 'UserName = "%s"' % self.user ## -## print "user_name_query: |%s|" % user_name_query +## print("user_name_query: |%s|" % user_name_query) ## + basic_user_fields = "ObjectID,UserName,DisplayName,FirstName,LastName,Disabled,UserProfile" try: timer_start = time.time() if self.user: - response = self.agent.get('User', fetch=True, query=user_name_query, _disableAugments=True) + response = self.agent.get('User', fetch=basic_user_fields, query=user_name_query, _disableAugments=True) else: - response = self.agent.get('User', fetch=True, _disableAugments=True) + response = self.agent.get('User', fetch=basic_user_fields, _disableAugments=True) timer_stop = time.time() except Exception as ex: ## - print "-----" - print str(ex) + print("-----") + print(str(ex)) ## if str(ex.args[0]).startswith('404 Service unavailable'): # TODO: discern whether we should mention server or target_host as the culprit - raise RallyRESTAPIError("hostname: '%s' non-existent or unreachable" % server) + raise RallyRESTAPIError("hostname: '%s' non-existent or unreachable" % self.server) else: raise elapsed = timer_stop - timer_start if response.status_code != 200: ## -## print "context check response:\n%s\n" % response -## print "request attempt elapsed time: %6.2f" % elapsed +## print("context check response:\n%s\n" % response) +## print("request attempt elapsed time: %6.2f" % elapsed) ## if response.status_code == 401: raise RallyRESTAPIError("Invalid credentials") if response.status_code == 404: +## +## print("response.errors: {0}".format(response.errors[0])) +## if elapsed >= float(INITIAL_REQUEST_TIME_LIMIT): - problem = "Request timed out on attempt to reach %s" % server - elif response.errors and 'certificate verify failed' in response.errors[0]: + problem = "Request timed out on attempt to reach %s" % self.server + elif response.errors and 'certificate verify failed' in str(response.errors[0]): problem = "SSL certificate verification failed" - elif response.errors and 'Max retries exceeded with url' in response.errors[0]: - problem = "Target Rally host: '%s' non-existent or unreachable" % server - elif response.errors and 'NoneType' in response.errors[0]: - problem = "Target Rally host: '%s' non-existent or unreachable" % server + elif response.errors and 'ProxyError' in str(response.errors[0]): + mo = re.search(r'ProxyError\((.+)\)$', response.errors[0]) + problem = mo.groups()[0][:-1] + problem = re.sub(r'NewConnectionError.+>:', '', problem)[:-3] + elif response.errors and 'Max retries exceeded with url' in str(response.errors[0]): + problem = "Target Rally host: '%s' non-existent or unreachable" % self.server + elif response.errors and 'NoneType' in str(response.errors[0]): + problem = "Target Rally host: '%s' non-existent or unreachable" % self.server else: sys.stderr.write("404 Response for request\n") - sys.stderr.write("\n".join(response.errors) + "\n") + #sys.stderr.write("\n".join(str(response.errors)) + "\n") if response.warnings: - sys.stderr.write("\n".join(response.warnings) + "\n") + sys.stderr.write("\n".join(str(response.warnings)) + "\n") sys.stderr.flush() - problem = "404 Target host: '%s' doesn't support the Rally WSAPI" % server + problem = "404 Target host: '%s' is either not reachable or doesn't support the Rally WSAPI" % self.server else: # might be a 401 No Authentication or 401 The username or password you entered is incorrect. ## -## print response.status_code -## print response.headers -## print response.errors +## print(response.status_code) +## print(response.headers) +## print(response.errors) ## - if 'The username or password you entered is incorrect.' in response.errors[0]: + if 'The username or password you entered is incorrect.' in str(response.errors[0]): problem = "Invalid credentials" else: error_blurb = response.errors[0][:80] if response.errors else "" problem = "%s %s" % (response.status_code, error_blurb) raise RallyRESTAPIError(problem) ## -## print " RallyContextHelper.check got the User info ..." -## print "response resource: %s" % response.resource -## print "response status code: %s" % response.status_code -## print "response headers: %s" % response.headers -## print "response errors: %s" % response.errors -## print "response warnings: %s" % response.warnings -## print "response resultCount: %s" % response.resultCount +## print(" RallyContextHelper.check -> _getUserInfo got the User info request response...") +## print("response resource: %s" % response.resource) +## print("response status code: %s" % response.status_code) +## print("response headers: %s" % response.headers) +## print("response errors: %s" % response.errors) +## print("response warnings: %s" % response.warnings) +## print("response resultCount: %s" % response.resultCount) ## sys.stdout.flush() ## - self._loadSubscription() - self._getDefaults(response) - self._getWorkspacesAndProjects(workspace=self._defaultWorkspace, project=self._defaultProject) - # TODO: slurp in all schema for the default workspace and get this into the entity.py - schema_info = self.agent.getSchemaInfo(self._defaultWorkspace) - processSchemaInfo(self.getWorkspace(), schema_info) - self.inflated = 'minimal' + return response + def _loadSubscription(self): sub = self.agent.get('Subscription', fetch=True, _disableAugments=True) @@ -233,21 +281,85 @@ def _loadSubscription(self): subscription = sub.next() self._subs_name = subscription.Name self.context.subs_name = subscription.Name - wksp_coll_ref_base = "%s/Workspaces" % subscription._ref - workspaces_collection_url = '%s?fetch=true&query=(State = Open)&pagesize=200&start_index=1' % wksp_coll_ref_base - workspaces = self.agent.getCollection(workspaces_collection_url, _disableAugments=True) - subscription.Workspaces = [wksp for wksp in workspaces] + return subscription + + + def _setOperatingContext(self, project_name): + """ + This is called after we've determined that there is access to what is now + in self._currentWorkspace. Query for projects in the self._currentWorkspace. + Set the self._defaultProject arbitrarily to the first Project.Name in the returned set, + and then thereafter reset that to the project_name parameter value if a match for that + exists in the returned set. If the project_name parameter is non-None and there is NOT + a match in the returned set raise an Exception stating that fact. + """ + result = self.agent.get('Project', fetch="Name", workspace=self._currentWorkspace) + + if not result or result.resultCount == 0: + problem = "No Projects found in the Workspace '%s'" % self._defaultWorkspace + raise RallyRESTAPIError(problem) + + try: + projects = [proj for proj in result] + except: + problem = "Unable to obtain Project Name values for projects in the '%s' Workspace" + raise RallyRESTAPIError(problem % self._defaultWorkspace) + + match_for_default_project = [project for project in projects if project.Name == self._defaultProject] + match_for_named_project = [project for project in projects if project.Name == project_name] + + if project_name: + if not match_for_named_project: + problem = "No valid Project with the name '%s' found in the Workspace '%s'" + raise RallyRESTAPIError(problem % (project_name, self._currentWorkspace)) + else: + project = match_for_named_project[0] + proj_ref = project._ref + self._defaultProject = project.Name + self._currentProject = project.Name + else: + if not match_for_default_project: + problem = "Default Project with the name '%s' was not found in the Workspace '%s'" + raise RallyRESTAPIError(problem % (self._defaultProject, self._defaultWorkspace)) + else: + project = match_for_default_project[0] + proj_ref = project._ref + self._defaultProject = project.Name + self._currentProject = project.Name ## -## num_wksps = len(subscription.Workspaces) -## print "Subscription has %d active Workspaces" % num_wksps +## print(" Default Workspace : %s" % self._defaultWorkspace) +## print(" Default Project : %s" % self._defaultProject) ## - self._subs_workspaces = subscription.Workspaces - self._defaultWorkspace = subscription.Workspaces[0] + if not self._workspaces: + self._workspaces = [self._defaultWorkspace] + if not self._projects: + self._projects = {self._defaultWorkspace : [self._defaultProject]} + if not self._workspace_ref: + short_ref = "/".join(wkspace_ref.split('/')[-2:]) # we only need the 'workspace/' part to be a valid ref + self._workspace_ref = {self._defaultWorkspace : short_ref} + if not self._project_ref: + short_ref = "/".join(proj_ref.split('/')[-2:]) # we only need the 'project/' part to be a valid ref + self._project_ref = {self._defaultWorkspace : {self._defaultProject : short_ref}} + self.defaultContext = RallyContext(self.server, + self.user, + self.password, + self.agent.serviceURL(), + subscription=self._subs_name, + workspace=self._defaultWorkspace, + project=self._defaultProject) + self.operatingContext = RallyContext(self.server, + self.user, + self.password, + self.agent.serviceURL(), + subscription=self._subs_name, + workspace=self._currentWorkspace, + project=self._currentProject) + self.context = self.operatingContext ## -## print "Subscription default Workspace: %s" % self._defaultWorkspace.Name +## print(" completed _setOperatingContext processing...") ## - def _getDefaults(self, response): + def _getDefaults(self, user_response): """ We have to circumvent the normal machinery as this is part of setting up the normal machinery. So, once having obtained the User object, we grab the @@ -255,89 +367,84 @@ def _getDefaults(self, response): and handling the response (wrapped in a RallyRESTResponse). """ ## -## print "in RallyContextHelper._getDefaults, response arg has:" +## print("in RallyContextHelper._getDefaults, response arg has:") ## #pprint(response.data[u'Results']) ## pprint(response.data) ## - user = response.next() + user = user_response.next() ## -## print user.details() +## pprint(response.data[u'Results'][0]) ## self.user_oid = user.oid ## -## print " RallyContextHelper._getDefaults calling _getResourceByOID to get UserProfile info..." +## print(" RallyContextHelper._getDefaults calling _getResourceByOID to get UserProfile info...") ## sys.stdout.flush() ## upraw = self.agent._getResourceByOID(self.context, 'UserProfile', user.UserProfile.oid, _disableAugments=True) ## -## print " RallyContextHelper._getDefaults got the raw UserProfile info via _getResourceByOID..." +## print(" RallyContextHelper._getDefaults got the raw UserProfile info via _getResourceByOID...") +## print(upraw.status_code) +## print(upraw.content) ## sys.stdout.flush() ## resp = RallyRESTResponse(self.agent, self.context, 'UserProfile', upraw, "full", 0) - up = resp.data[u'QueryResult'][u'Results']['UserProfile'] + up = resp.data['QueryResult']['Results']['UserProfile'] ## -## print "got the UserProfile info..." +## print("got the UserProfile info...") ## pprint(up) -## print "+" * 80 +## print("+" * 80) ## if up['DefaultWorkspace']: self._defaultWorkspace = up['DefaultWorkspace']['_refObjectName'] ## -## print " set _defaultWorkspace to: %s" % self._defaultWorkspace +## print(" set _defaultWorkspace to: %s" % self._defaultWorkspace) ## - self._currentWorkspace = self._defaultWorkspace + self._currentWorkspace = self._defaultWorkspace[:] wkspace_ref = up['DefaultWorkspace']['_ref'] else: - self._currentWorkspace = self._defaultWorkspace.Name - wkspace_ref = self._defaultWorkspace._ref - self._defaultWorkspace = self._defaultWorkspace.Name + self._defaultWorkspace = None + self._currentWorkspace = None + wkspace_ref = None if up['DefaultProject']: self._defaultProject = up['DefaultProject']['_refObjectName'] - self._currentProject = self._defaultProject + self._currentProject = self._defaultProject[:] proj_ref = up['DefaultProject']['_ref'] else: - self._defaultProject = "" - self._currentProject = "" - proj_ref = "" - projects = self.agent.get('Project', fetch="Name", workspace=self._defaultWorkspace) + self._defaultProject = None + self._currentProject = None + proj_ref = None ## -## print projects.content +## print(" Default Workspace : %s" % self._defaultWorkspace) +## print(" Default Project : %s" % self._defaultProject) ## - if projects: - try: - proj = projects.next() - proj_ref = proj._ref - self._defaultProject = proj.Name - self._currentProject = proj.Name - except StopIteration: # the default Workspace might not have any projects - pass + + + def _getSubscriptionWorkspaces(self, subscription, workspace=None, limit=0): + wksp_coll_ref_base = "%s/Workspaces" % subscription._ref + criteria = "(State = Open)" + # if workspace then augment the query + if isinstance(workspace, str) and len(workspace) > 0: + criteria = '((Name = "%s") AND %s)' % (workspace, criteria) + workspaces_collection_url = '%s?fetch=true&query=%s&pagesize=200&start=1' % \ + (wksp_coll_ref_base, criteria) + timer_start = time.time() + workspaces = self.agent.getCollection(workspaces_collection_url, _disableAugments=True) + timer_stop = time.time() + elapsed = timer_stop - timer_start ## -## print " Default Workspace : %s" % self._defaultWorkspace -## print " Default Project : %s" % self._defaultProject +## print("getting the Workspace collection took %5.3f seconds" % elapsed) ## - - if not self._workspaces: - self._workspaces = [self._defaultWorkspace] - if not self._projects: - self._projects = {self._defaultWorkspace : [self._defaultProject]} - if not self._workspace_ref: - short_ref = "/".join(wkspace_ref.split('/')[-2:]) # we only need the 'workspace/' part to be a valid ref - self._workspace_ref = {self._defaultWorkspace : short_ref} - if not self._project_ref: - short_ref = "/".join(proj_ref.split('/')[-2:]) # we only need the 'project/' part to be a valid ref - self._project_ref = {self._defaultWorkspace : {self._defaultProject : short_ref}} - self.defaultContext = RallyContext(self.server, - self.user, - self.password, - self.agent.serviceURL(), - subscription=self._subs_name, - workspace=self._defaultWorkspace, - project=self._defaultProject) - self.context = self.defaultContext + subscription.Workspaces = [wksp for wksp in workspaces] +## +## num_wksps = len(subscription.Workspaces) +## if not limit: print("Subscription %s has %d active Workspaces" % (subscription.Name, num_wksps)) ## -## print " completed _getDefaults processing..." + self._subs_workspaces = subscription.Workspaces +## +## print("Subscription default Workspace: %s" % self._defaultWorkspace.Name) ## + return subscription.Workspaces def currentContext(self): @@ -345,7 +452,7 @@ def currentContext(self): def setWorkspace(self, workspace_name): ## -## print "in setWorkspace, exising workspace: %s OID: %s" % (self._currentWorkspace, self.currentWorkspaceRef()) +## print("in setWorkspace, exising workspace: %s OID: %s" % (self._currentWorkspace, self.currentWorkspaceRef())) ## if self.isAccessibleWorkspaceName(workspace_name): if workspace_name not in self._workspaces: @@ -354,17 +461,17 @@ def setWorkspace(self, workspace_name): self._currentWorkspace = workspace_name self.context.workspace = workspace_name ## -## print " current workspace set to: %s OID: %s" % (workspace_name, self.currentWorkspaceRef()) +## print(" current workspace set to: %s OID: %s" % (workspace_name, self.currentWorkspaceRef())) ## self.resetDefaultProject() ## -## print " context project set to: %s" % self._currentProject +## print(" context project set to: %s" % self._currentProject) ## try: # make sure that entity._rally_schema gets filled for this workspace # this will fault and be caught if getSchemaItem raises an Exception getSchemaItem(self.getWorkspace(), 'Defect') - except Exception, msg: + except Exception as msg: schema_info = self.agent.getSchemaInfo(self.getWorkspace()) processSchemaInfo(self.getWorkspace(), schema_info) else: @@ -416,8 +523,8 @@ def currentWorkspaceRef(self): Return the ref associated with the current workspace if you can find one """ ## -## print "default workspace: %s" % self._defaultWorkspace -## print "current workspace: %s" % self._currentWorkspace +## print("default workspace: %s" % self._defaultWorkspace) +## print("current workspace: %s" % self._currentWorkspace) ## if self._currentWorkspace: return self._workspace_ref[self._currentWorkspace] @@ -461,10 +568,10 @@ def getAccessibleProjects(self, workspace='default'): if workspace not in self._workspaces: return projectInfo ## else: -## print " self._workspaces augmented, now has your target workspace" +## print(" self._workspaces augmented, now has your target workspace") ## sys.stdout.flush() ## - for projName, projRef in self._project_ref[workspace].items(): + for projName, projRef in list(self._project_ref[workspace].items()): projectInfo.append((projName, projRef)) return projectInfo @@ -482,12 +589,12 @@ def resetDefaultProject(self): current_valid_projects = self.getAccessibleProjects(self._currentWorkspace) proj_names = sorted([name for name, ref in current_valid_projects]) proj_refs = self._project_ref[self._currentWorkspace] - if unicode(self._defaultProject) in proj_names and unicode(self._currentProject) in proj_names: + if str(self._defaultProject) in proj_names and str(self._currentProject) in proj_names: return (self._defaultProject, proj_refs[self._defaultProject]) - if unicode(self._defaultProject) not in proj_names: + if str(self._defaultProject) not in proj_names: self._defaultProject = proj_names[0] - if unicode(self._currentProject) not in proj_names: + if str(self._currentProject) not in proj_names: self.setProject(self._defaultProject) return (self._defaultProject, proj_refs[self._defaultProject]) @@ -501,14 +608,12 @@ def currentProjectRef(self): return "" if not self._currentProject: return "" - ## -## print " currentProjectRef() ... " -## print " _currentWorkspace: '%s'" % self._currentWorkspace -## print " _currentProject : '%s'" % self._currentProject -## print " _project_ref keys: %s" % repr(self._project_ref.keys()) +## print(" currentProjectRef() ... ") +## print(" _currentWorkspace: '%s'" % self._currentWorkspace) +## print(" _currentProject : '%s'" % self._currentProject) +## print(" _project_ref keys: %s" % repr(self._project_ref.keys())) ## - # # this next condition could be True in limited circumstances, like on initialization # when info for the _currentProject hasn't yet been retrieved, @@ -531,7 +636,7 @@ def _establishContext(self, kwargs): if kwargs and 'project' in kwargs: project = kwargs['project'] ## -## print "_establishContext calling _getWorkspacesAndProjects(workspace=%s, project=%s)" % (workspace, project) +## print("_establishContext calling _getWorkspacesAndProjects(workspace=%s, project=%s)" % (workspace, project)) ## self._getWorkspacesAndProjects(workspace=workspace, project=project) if workspace: @@ -544,7 +649,7 @@ def identifyContext(self, **kwargs): Return back a tuple of (RallyContext instance, augment list with hrefs) """ ## -## print "... RallyContextHelper.identifyContext kwargs: %s" % repr(kwargs) +## print("... RallyContextHelper.identifyContext kwargs: %s" % repr(kwargs)) ## sys.stdout.flush() ## augments = [] @@ -563,7 +668,7 @@ def identifyContext(self, **kwargs): if workspace not in eligible_workspace_names: problem = 'Workspace specified: "%s" not accessible with current credentials' - raise RallyRESTAPIError(problem % workspace) + raise RallyRESTAPIError(problem % workspace.Name) if workspace not in self._workspaces and self._inflated != 'wide': ec_kwargs = {'workspace' : workspace} self._establishContext(ec_kwargs) @@ -605,8 +710,13 @@ def identifyContext(self, **kwargs): augments.append("projectScopeDown=true") if not workspace and project: - self.context = self.defaultContext + self.context = self.operatingContext + # check to see if the _current_project is actually in the _current_workspace + if self._currentProject not in self._projects[self._currentWorkspace]: + problem = "the current Workspace |%s| does not contain a Project that matches the current setting of the Project: %s" % (self._currentWorkspace, self._currentProject) + raise RallyRESTAPIError(problem) +## return self.context, augments @@ -620,12 +730,11 @@ def _getWorkspacesAndProjects(self, **kwargs): if target_workspace == '*': # wild card value to specify all workspaces target_workspace = None ## -## print "in _getWorkspacesAndProjects(%s)" % repr(kwargs) -## print "_getWorkspacesAndProjects, target_workspace: %s" % target_workspace -## print "_getWorkspacesAndProjects, self._currentWorkspace: %s" % self._currentWorkspace -## print "_getWorkspacesAndProjects, self._defaultWorkspace: %s" % self._defaultWorkspace +## print("in _getWorkspacesAndProjects(%s)" % repr(kwargs)) +## print("_getWorkspacesAndProjects, target_workspace: %s" % target_workspace) +## print("_getWorkspacesAndProjects, self._currentWorkspace: %s" % self._currentWorkspace) +## print("_getWorkspacesAndProjects, self._defaultWorkspace: %s" % self._defaultWorkspace) ## - for workspace in self._subs_workspaces: # short-circuit issuing any WS calls if we don't need to if target_workspace and workspace.Name != target_workspace: @@ -633,7 +742,7 @@ def _getWorkspacesAndProjects(self, **kwargs): if self._workspace_inflated.get(workspace.Name, False) == True: continue ## -## print workspace.Name, workspace.oid +## print(workspace.Name, workspace.oid) ## # fill out self._workspaces and self._workspace_ref if workspace.Name not in self._workspaces: @@ -643,19 +752,19 @@ def _getWorkspacesAndProjects(self, **kwargs): self._projects[ workspace.Name] = [] self._project_ref[ workspace.Name] = {} resp = self.agent._getResourceByOID( self.context, 'workspace', workspace.oid, _disableAugments=True) - response = json.loads(resp.content) + response = resp.json() # If SLM gave back consistent responses, we could use RallyRESTResponse, but no joy... # Carefully weasel into the response to get to the guts of what we need # and note we specify only the necessary fetch fields or this query takes a *lot* longer... - base_proj_coll_url = response['Workspace']['Projects'][u'_ref'] - projects_collection_url = '%s?fetch="ObjectID,Name,State"&pagesize=200&start_index=1' % base_proj_coll_url + base_proj_coll_url = response['Workspace']['Projects']['_ref'] + projects_collection_url = '%s?fetch="ObjectID,Name,State"&pagesize=200&start=1' % base_proj_coll_url response = self.agent.getCollection(projects_collection_url, _disableAugments=True) #not-as-bad?# response = self.agent.get('Project', fetch="ObjectID,Name,State", workspace=workspace.Name) ## -## print " Number of Projects: %d" % response.data[u'TotalResultCount'] +## print(" Number of Projects: %d" % response.data[u'TotalResultCount']) ## for item in response.data[u'Results']: -## print " %-36.36s" % (item[u'_refObjectName'], ) +## print(" %-36.36s" % (item[u'_refObjectName'], )) ## for project in response: projName = project.Name @@ -670,12 +779,12 @@ def _getWorkspacesAndProjects(self, **kwargs): if 'workspace' in kwargs and kwargs['workspace']: self._inflated = 'narrow' ## -## print "setting _inflated to 'narrow'" +## print("setting _inflated to 'narrow'") ## else: self._inflated = 'wide' ## -## print "setting _inflated to 'wide'" +## print("setting _inflated to 'wide'") ## def getSchemaItem(self, entity_name): @@ -686,6 +795,7 @@ def __repr__(self): items = [] items.append('%s = %s' % ('server', self.server)) items.append('%s = %s' % ('defaultContext', self.defaultContext)) + items.append('%s = %s' % ('operatingContext', self.operatingContext)) items.append('%s = %s' % ('_subs_name', self._subs_name)) items.append('%s = %s' % ('_workspaces', repr(self._workspaces))) items.append('%s = %s' % ('_projects', repr(self._projects))) @@ -706,15 +816,26 @@ class Pinger(object): Response to the ping command results in the ping method returning True, otherwise a False is returned """ - PING_COMMAND = {'Darwin' : ["ping", "-o", "-c", "2", "-t", "2"], - 'Unix' : ["ping", "-c", "2", "-w", "2"], - 'Linux' : ["ping", "-c", "2", "-w", "2"], - 'Windows' : ["ping", "-n", "2", "-w", "2"], - 'Cygwin' : ["ping", "-n", "2", "-w", "2"] + called = False + MAX_PING_ATTEMPTS = 2 + DEFAULT_PING_TIMEOUT = 6 + ping_timeout = os.getenv('PYRAL_PING_TIMEOUT', None) + if ping_timeout: + result = re.match(r'^(?P\d+)$', ping_timeout) + ping_timeout = result.groupdict('digits') if result else DEFAULT_PING_TIMEOUT + else: + ping_timeout = DEFAULT_PING_TIMEOUT + + PING_COMMAND = {'Darwin' : ["ping", "-o", "-c", str(MAX_PING_ATTEMPTS), "-t", str(ping_timeout)], + 'Unix' : ["ping", "-c", str(MAX_PING_ATTEMPTS), "-w", str(ping_timeout)], + 'Linux' : ["ping", "-c", str(MAX_PING_ATTEMPTS), "-w", str(ping_timeout)], + 'Windows' : ["ping", "-n", str(MAX_PING_ATTEMPTS), "-w", str(ping_timeout)], + 'Cygwin' : ["ping", "-n", str(MAX_PING_ATTEMPTS), "-w", str(ping_timeout)] } @classmethod def ping(self, target): + Pinger.called = True plat_ident = platform.system() if plat_ident.startswith('CYGWIN'): plat_ident = 'Cygwin' @@ -722,11 +843,13 @@ def ping(self, target): vector.append(target) bucket = ".ping-bucket" result = "" + rc = -1 try: with open(bucket, 'w') as sink: rc = subprocess.call(vector, stdout=sink, stderr=sink) except: stuff = sys.exc_info() + #print(stuff) result = stuff[1] finally: with open(bucket, 'r') as of: diff --git a/pyral/entity.py b/pyral/entity.py index 2a8b006..01b2cfc 100644 --- a/pyral/entity.py +++ b/pyral/entity.py @@ -8,11 +8,12 @@ # ################################################################################################### -__version__ = (1, 1, 1) +__version__ = (1, 2, 0) import sys import re import types +import time from .restapi import hydrateAnInstance from .restapi import getResourceByOID @@ -66,8 +67,13 @@ def __init__(self, oid, name, resource_url, context): """ All sub-classes have an oid (Object ID), so it makes sense to provide the attribute storage here. + All sub-classes also have a uuid (ObjectUUID), so it also makes sense + to provide the attribute storage here. """ - self.oid = oid + try: + self.oid = int(oid) + except: + self.oid = oid self.Name = name self._ref = resource_url self._hydrated = False @@ -109,7 +115,7 @@ def __getattr__(self, name): faultTrigger = "getattr fault detected on %s instance for attribute: %s (hydrated? %s)" % \ (rallyEntityTypeName, name, self._hydrated) ## -## print faultTrigger +## print(faultTrigger) ## sys.stdout.flush() ## if not self._hydrated: @@ -119,9 +125,9 @@ def __getattr__(self, name): # entity_name, oid = self._ref.split(SLM_WS_VER)[-1].rsplit('/', 1) ## -## print "self._ref : %s" % self._ref -## print "issuing OID specific get for %s OID: %s " % (entity_name, oid) -## print "Entity: %s context: %s" % (rallyEntityTypeName, self._context) +## print("self._ref : %s" % self._ref) +## print("issuing OID specific get for %s OID: %s " % (entity_name, oid)) +## print("Entity: %s context: %s" % (rallyEntityTypeName, self._context)) ## sys.stdout.flush() ## response = getResourceByOID(self._context, entity_name, self.oid, unwrap=True) @@ -129,11 +135,11 @@ def __getattr__(self, name): raise UnreferenceableOIDError("%s OID %s" % (rallyEntityTypeName, self.oid)) if not isinstance(response, object): # TODO: would like to be specific with RallyRESTResponse here... - #print "bad guess on response type in __getattr__, response is a %s" % type(response) + #print("bad guess on response type in __getattr__, response is a %s" % type(response)) raise UnreferenceableOIDError("%s OID %s" % (rallyEntityTypeName, self.oid)) if response.status_code != 200: ## -## print response +## print(response) ## raise UnreferenceableOIDError("%s OID %s" % (rallyEntityTypeName, self.oid)) @@ -151,11 +157,11 @@ def __getattr__(self, name): # upon initial access of a Collection type field, we have to detect, retrieve the Collection # and then torch the "lazy" evaluation field marker coll_ref_field = '__collection_ref_for_%s' % name - if coll_ref_field in self.__dict__.keys(): + if coll_ref_field in list(self.__dict__.keys()): collection_ref = self.__dict__[coll_ref_field] ## -## print " chasing %s collection ref: %s" % (name, collection_ref) -## print " using this context: %s" % repr(self._context) +## print(" chasing %s collection ref: %s" % (name, collection_ref)) +## print(" using this context: %s" % repr(self._context)) ## collection = getCollection(self._context, collection_ref, _disableAugments=False) if name != "RevisionHistory": # a "normal" Collections field ... @@ -167,7 +173,7 @@ def __getattr__(self, name): else: description = "%s instance has no attribute: '%s'" % (rallyEntityTypeName, name) ## -## print "Rally entity getattr fault: %s" % description +## print("Rally entity getattr fault: %s" % description) ## raise AttributeError(description) @@ -180,9 +186,9 @@ def _hydrateRevisionHistory(self, collection_ref, collection): so that is retrieved and used to construct the "guts" of RevisionHistory, ie., the Revisions. """ # pull the necessary fragment out from collection query, - rev_hist_raw = collection.data[u'QueryResult'][u'Results']['RevisionHistory'] - rev_hist_oid = rev_hist_raw[u'ObjectID'] - revs_ref = rev_hist_raw[u'Revisions'][u'_ref'] # this is the "true" Revisions collection ref + rev_hist_raw = collection.data['QueryResult']['Results']['RevisionHistory'] + rev_hist_oid = rev_hist_raw['ObjectID'] + revs_ref = rev_hist_raw['Revisions']['_ref'] # this is the "true" Revisions collection ref # create a RevisionHistory instance with oid, Name and _ref field information rev_hist = RevisionHistory(rev_hist_oid, 'RevisonHistory', collection_ref, self._context) # chase the revs_ref set the RevisionHistory.Revisions attribute with that Revisions collection @@ -208,7 +214,7 @@ class DomainObject(Persistable): pass class User (DomainObject): - USER_ATTRIBUTES = ['oid', 'ref', 'ObjectID', '_ref', + USER_ATTRIBUTES = ['oid', 'ref', 'ObjectID', 'ObjectUUID', '_ref', '_CreatedAt', '_hydrated', 'UserName', 'DisplayName', 'EmailAddress', 'FirstName', 'MiddleName', 'LastName', @@ -248,7 +254,7 @@ def details(self): return "\n".join(tank) class UserProfile (DomainObject): - USER_PROFILE_ATTRIBUTES = ['oid', 'ref', 'ObjectID', '_ref', + USER_PROFILE_ATTRIBUTES = ['oid', 'ref', 'ObjectID', 'ObjectUUID', '_ref', '_CreatedAt', '_hydrated', 'DefaultWorkspace', 'DefaultProject', 'TimeZone', @@ -297,7 +303,7 @@ class WorkspaceDomainObject(DomainObject): mulitiline string representation. """ COMMON_ATTRIBUTES = ['_type', - 'oid', 'ref', 'ObjectID', '_ref', + 'oid', 'ref', 'ObjectID', 'ObjectUUID', '_ref', '_CreatedAt', '_hydrated', 'Name', 'Subscription', 'Workspace', 'FormattedID' @@ -314,6 +320,7 @@ def details(self): _hydrated _CreatedAt ObjectID + ObjectUUID Name ** not all items will have this... Subscription (oid, Name) Workspace (oid, Name) @@ -342,6 +349,9 @@ def details(self): tank.append(anv) tank.append("") other_attributes = set(self.attributes()) - set(self.COMMON_ATTRIBUTES) +## +## print("other_attributes: %s" % ", ".join(other_attributes)) +## for attribute_name in sorted(other_attributes): #value = getattr(self, attribute_name) # @@ -350,6 +360,9 @@ def details(self): try: value = getattr(self, attribute_name) except AttributeError: +## +## print(" unable to getattr for |%s|" % attribute_name) +## continue attr_name = attribute_name if attribute_name.startswith('c_'): @@ -466,7 +479,7 @@ class CustomField(object): def __init__(self, oid, name, resource_url, context): """ """ - self.oid = oid + self.oid = int(oid) self.Name = name self._ref = resource_url self._context = context @@ -501,7 +514,7 @@ def __init__(self, oid, name, resource_url, context): All sub-classes have an oid (Object ID), so it makes sense to provide the attribute storage here. """ - self.oid = self.ObjectID = oid + self.oid = self.ObjectID = int(oid) self.Name = name self._ref = resource_url self._hydrated = True @@ -592,7 +605,7 @@ def __setattr__(self, item, value): 'SearchObject' : SearchObject, } -for entity_name, entity_class in classFor.items(): +for entity_name, entity_class in list(classFor.items()): _rally_entity_cache[entity_name] = entity_name entity_class = None # reset... @@ -622,26 +635,26 @@ def __init__(self, raw_info): # Attributes # RevisionHistory # Subscription, Workspace - self.ref = "/".join(raw_info[u'_ref'].split('/')[-2:]) - self.ObjectName = str(raw_info[u'_refObjectName']) - self.ElementName = str(raw_info[u'ElementName']) - self.Name = str(raw_info[u'Name']) - self.DisplayName = str(raw_info[u'DisplayName']) - self.TypePath = str(raw_info[u'TypePath']) - self.IDPrefix = str(raw_info[u'IDPrefix']) - self.Abstract = raw_info[u'Abstract'] - self.Parent = raw_info[u'Parent'] + self.ref = "/".join(raw_info['_ref'].split('/')[-2:]) + self.ObjectName = str(raw_info['_refObjectName']) + self.ElementName = str(raw_info['ElementName']) + self.Name = str(raw_info['Name']) + self.DisplayName = str(raw_info['DisplayName']) + self.TypePath = str(raw_info['TypePath']) + self.IDPrefix = str(raw_info['IDPrefix']) + self.Abstract = raw_info['Abstract'] + self.Parent = raw_info['Parent'] if self.Parent: # so apparently AdministratableProject doesn't have a Parent object - self.Parent = str(self.Parent[u'_refObjectName']) - self.Creatable = raw_info[u'Creatable'] - self.Queryable = raw_info[u'Queryable'] - self.ReadOnly = raw_info[u'ReadOnly'] - self.Deletable = raw_info[u'Deletable'] - self.Restorable = raw_info[u'Restorable'] - self.Ordinal = raw_info[u'Ordinal'] - self.RevisionHistory = raw_info[u'RevisionHistory'] # a ref to a Collection, defer chasing for now... + self.Parent = str(self.Parent['_refObjectName']) + self.Creatable = raw_info['Creatable'] + self.Queryable = raw_info['Queryable'] + self.ReadOnly = raw_info['ReadOnly'] + self.Deletable = raw_info['Deletable'] + self.Restorable = raw_info['Restorable'] + self.Ordinal = raw_info['Ordinal'] + self.RevisionHistory = raw_info['RevisionHistory'] # a ref to a Collection, defer chasing for now... self.Attributes = [] - for attr in raw_info[u'Attributes']: + for attr in raw_info['Attributes']: self.Attributes.append(SchemaItemAttribute(attr)) self.completed = False @@ -655,7 +668,8 @@ def complete(self, context, getCollection): """ if self.completed: return True - for attribute in self.Attributes: + for attribute in sorted([attr for attr in self.Attributes if attr.AttributeType in ['RATING', 'STATE']]): + # only an attribute whose AttributeType is RATING or STATE will have allowedValues attribute.resolveAllowedValues(context, getCollection) self.completed = True @@ -709,34 +723,34 @@ def __str__(self): class SchemaItemAttribute(object): def __init__(self, attr_info): self._type = "AttributeDefinition" - self.ref = "/".join(attr_info[u'_ref'][-2:]) - self.ObjectName = str(attr_info[u'_refObjectName']) - self.ElementName = str(attr_info[u'ElementName']) - self.Name = str(attr_info[u'Name']) - self.AttributeType = str(attr_info[u'AttributeType']) - self.Subscription = attr_info[u'Subscription'] - self.Workspace = attr_info[u'Workspace'] - self.Custom = attr_info[u'Custom'] - self.Required = attr_info[u'Required'] - self.ReadOnly = attr_info[u'ReadOnly'] - self.Filterable = attr_info[u'Filterable'] - self.Hidden = attr_info[u'Hidden'] - self.SchemaType = attr_info[u'SchemaType'] - self.Constrained = attr_info[u'Constrained'] - self.AllowedValueType = attr_info[u'AllowedValueType'] # has value iff this attribute has allowed values - self.AllowedValues = attr_info[u'AllowedValues'] - self.MaxLength = attr_info[u'MaxLength'] - self.MaxFractionalDigits = attr_info[u'MaxFractionalDigits'] - if self.AllowedValues and type(self.AllowedValues) == types.DictType: - self.AllowedValues = str(self.AllowedValues[u'_ref']) # take the ref as value + self.ref = "/".join(attr_info['_ref'][-2:]) + self.ObjectName = str(attr_info['_refObjectName']) + self.ElementName = str(attr_info['ElementName']) + self.Name = str(attr_info['Name']) + self.AttributeType = str(attr_info['AttributeType']) + self.Subscription = attr_info.get('Subscription', None) # not having 'Subscription' should be rare + self.Workspace = attr_info.get('Workspace', None) # apparently only custom fields will have a 'Workspace' value + self.Custom = attr_info['Custom'] + self.Required = attr_info['Required'] + self.ReadOnly = attr_info['ReadOnly'] + self.Filterable = attr_info['Filterable'] + self.Hidden = attr_info['Hidden'] + self.SchemaType = attr_info['SchemaType'] + self.Constrained = attr_info['Constrained'] + self.AllowedValueType = attr_info['AllowedValueType'] # has value iff this attribute has allowed values + self.AllowedValues = attr_info['AllowedValues'] + self.MaxLength = attr_info['MaxLength'] + self.MaxFractionalDigits = attr_info['MaxFractionalDigits'] + if self.AllowedValues and type(self.AllowedValues) == dict: + self.AllowedValues = str(self.AllowedValues['_ref']) # take the ref as value self._allowed_values = True self._allowed_values_resolved = False - elif self.AllowedValues and type(self.AllowedValues) == types.ListType: + elif self.AllowedValues and type(self.AllowedValues) == list: buffer = [] for item in self.AllowedValues: - aav = AllowedAttributeValue(0, item[u'StringValue'], None, None) - aav.Name = item[u'StringValue'] - aav.StringValue = item[u'StringValue'] + aav = AllowedAttributeValue(0, item['StringValue'], None, None) + aav.Name = item['StringValue'] + aav.StringValue = item['StringValue'] aav._hydrated = True buffer.append(aav) self.AllowedValues = buffer[:] @@ -745,6 +759,8 @@ def __init__(self, attr_info): else: self._allowed_values = False + def __lt__(self, other): + return self.ElementName < other.ElementName def resolveAllowedValues(self, context, getCollection): """ @@ -754,12 +770,16 @@ def resolveAllowedValues(self, context, getCollection): The need to use getCollection is based on whether the AllowedValues value is a string that matches the regex '^https?://.*/attributedefinition/-\d+/AllowedValues' """ +## +## print("in resolveAllowedValues for |%s| is a %s" % (self.Name, type(self.Name))) +## print("in resolveAllowedValues for %s AllowedValues: %s" % (self.Name, self.AllowedValues)) +## if not self._allowed_values: self._allowed_values_resolved = True return True if self._allowed_values_resolved: return True - if type(self.AllowedValues) != types.StringType: + if type(self.AllowedValues) != str: #previously was != bytes return True std_av_ref_pattern = '^https?://.*/\w+/-?\d+/AllowedValues$' mo = re.match(std_av_ref_pattern, self.AllowedValues) @@ -767,7 +787,9 @@ def resolveAllowedValues(self, context, getCollection): anomaly = "Standard AllowedValues ref pattern |%s| not matched by candidate |%s|" % \ (std_av_ref_pattern, self.AllowedValues) raise UnrecognizedAllowedValuesReference(anomaly) - +## +## print("about to call getCollection for %s on %s" % (self.AllowedValues, self.Name)) +## collection = getCollection(context, self.AllowedValues) self.AllowedValues = [value for value in collection] self._allowed_values_resolved = True @@ -792,20 +814,20 @@ def __str__(self): output_lines = [ident_line, misc_line] if self.AllowedValueType and not self._allowed_values_resolved: - avt_ref = "/".join(self.AllowedValueType[u'_ref'].split('/')[-2:]) + avt_ref = "/".join(self.AllowedValueType['_ref'].split('/')[-2:]) avt_line = " AllowedValueType ref: %s" % avt_ref #output_lines.append(avt_line) avv_ref = "/".join(self.AllowedValues.split('/')[-3:]) avv_line = " AllowedValues: %s" % avv_ref output_lines.append(avv_line) elif self._allowed_values_resolved: - if self.AllowedValues and type(self.AllowedValues) == types.ListType: + if self.AllowedValues and type(self.AllowedValues) == list: avs = [] for ix, item in enumerate(self.AllowedValues): - if type(item) == types.DictType: - avs.append(str(item[u'StringValue'])) + if type(item) == dict: + avs.append(str(item['StringValue'])) else: - avs.append(str(item.__dict__[u'StringValue'])) + avs.append(str(item.__dict__['StringValue'])) avv_line = " AllowedValues: %s" % avs output_lines.append(avv_line) @@ -822,10 +844,10 @@ def getEntityName(candidate): global _rally_entity_cache official_name = candidate - hits = [path for entity, path in _rally_entity_cache.items() + hits = [path for entity, path in list(_rally_entity_cache.items()) if '/' in path and path.split('/')[1] == candidate] ## -## print "for candidate |%s| hits: |%s|" % (candidate, hits) +## print("for candidate |%s| hits: |%s|" % (candidate, hits)) ## if hits: official_name = hits.pop(0) @@ -869,17 +891,17 @@ def processSchemaInfo(workspace, schema_info): _rally_schema[wksp_ref][item.ElementName] = item if item.Abstract: continue - if not _rally_entity_cache.has_key(item.ElementName): + if item.ElementName not in _rally_entity_cache: _rally_entity_cache[item.ElementName] = item.ElementName if item.TypePath != item.ElementName: _rally_schema[wksp_ref][item.TypePath] = item - if not _rally_entity_cache.has_key(item.TypePath): + if item.TypePath not in _rally_entity_cache: _rally_entity_cache[item.TypePath] = item.TypePath _rally_schema[wksp_ref]['Story'] = _rally_schema[wksp_ref]['HierarchicalRequirement'] _rally_schema[wksp_ref]['UserStory'] = _rally_schema[wksp_ref]['HierarchicalRequirement'] - unaccounted_for_entities = [entity_name for entity_name in _rally_schema[wksp_ref].keys() - if not classFor.has_key(entity_name) + unaccounted_for_entities = [entity_name for entity_name in list(_rally_schema[wksp_ref].keys()) + if entity_name not in classFor and not entity_name.startswith('ObjectAttr') ] for entity_name in unaccounted_for_entities: @@ -889,7 +911,7 @@ def processSchemaInfo(workspace, schema_info): entity = _rally_schema[wksp_ref][entity_name] typePath = entity.TypePath pyralized_class_name = str(typePath.replace('/', '_')) - if not classFor.has_key(pyralized_class_name): + if pyralized_class_name not in classFor: parentClass = WorkspaceDomainObject if entity.Parent: try: @@ -906,7 +928,7 @@ def getSchemaItem(workspace, entity_name): if wksp_ref not in _rally_schema: raise Exception("Fault: no _rally_schema info for %s" % wksp_ref) schema = _rally_schema[wksp_ref] - if not schema.has_key(entity_name): + if entity_name not in schema: return None return schema[entity_name] diff --git a/pyral/hydrate.py b/pyral/hydrate.py index ccbd4cf..2d32bb7 100644 --- a/pyral/hydrate.py +++ b/pyral/hydrate.py @@ -9,14 +9,13 @@ # ################################################################################################### -__version__ = (1, 1, 1) +__version__ = (1, 2, 0) import sys -reload(sys) # Reload gets a sys module that has the setdefaultencoding before site.py deletes it -sys.setdefaultencoding('UTF8') - -import types -from pprint import pprint +import imp +imp.reload(sys) # Reload gets a sys module that has the setdefaultencoding before site.py deletes it +import six +six.PY2 and sys.setdefaultencoding('UTF8') # not required in python 3 from .entity import classFor, VERSION_ATTRIBUTES, MINIMAL_ATTRIBUTES, PORTFOLIO_ITEM_SUB_TYPES @@ -38,12 +37,7 @@ def __init__(self, context, hydration="full"): def _attributes(self, item): -## -## print "in hydrateInstance, item contents:" -## pprint(item) -## print "" -## - return [attr for attr in item.keys() + return [attr for attr in list(item.keys()) if attr not in MINIMAL_ATTRIBUTES and attr not in VERSION_ATTRIBUTES] @@ -70,8 +64,8 @@ def hydrateInstance(self, item, existingInstance=None): if self.hydration == "full": instance._hydrated = True ## -## print "hydrated %s has these attributes: %s, hydration setting: %s" % \ -## (instance._type, instance.attributes(), self.hydration) +## print("hydrated %s has these attributes: %s, hydration setting: %s" % \ +## (instance._type, instance.attributes(), self.hydration)) ## return instance @@ -82,20 +76,20 @@ def _basicInstance(self, item): However, there are entities with attributes that are non-scalar and do not have a '_type' entry. So, we cheat and make an instance of a CustomField class and return that. """ - itemType = item.get(u'_type', "CustomField") + itemType = item.get('_type', "CustomField") ## -## print "in EntityHydrator.hydrateInstance, _basicInstance to create a %s" % itemType +## print("in EntityHydrator.hydrateInstance, _basicInstance to create a %s for %s" % (itemType, item)) ## - name = item.get(u'_refObjectName', "Unknown") + name = item.get('_refObjectName', "Unknown") if itemType == 'AllowedQueryOperator': - name = item[u'OperatorName'] + name = item['OperatorName'] oid = 0 - resource_url = item.get(u'_ref', "") + resource_url = item.get('_ref', "") if resource_url: oid = resource_url.split('/')[-1] try: instance = classFor[str(itemType)](oid, name, resource_url, self.context) - except KeyError, e: + except KeyError as e: bonked = True if '/' in itemType: # valid after intro of dyna-types in 1.37 try: @@ -103,7 +97,7 @@ def _basicInstance(self, item): instance = classFor[str(type_name)](oid, name, resource_url, self.context) itemType = type_name bonked = False - except KeyError, e: + except KeyError as e: raise elif itemType in PORTFOLIO_ITEM_SUB_TYPES: try: @@ -111,45 +105,45 @@ def _basicInstance(self, item): instance = classFor[str(type_name)](oid, name, resource_url, self.context) itemType = type_name bonked = False - except KeyError, e: + except KeyError as e: raise if bonked: - print "No classFor item for |%s|" % itemType + print("No classFor item for |%s|" % itemType) raise KeyError(itemType) instance._type = itemType # although, this info is also available via instance.__class__.__name__ if itemType == 'AllowedAttributeValue': instance.Name = 'AllowedValue' - instance.value = item[u'StringValue'] + instance.value = item['StringValue'] ## -## print "in EntityHydrator.hydrateInstance, _basicInstance returning a %s" % instance._type +## print("in EntityHydrator.hydrateInstance, _basicInstance returning a %s" % instance._type) ## return instance def _setAppropriateAttrValueForType(self, instance, attrName, attrValue, level=0): ## ## indent = " " * level -## print "%s attr level: %d attrName |%s| attrValue: |%s|" % (indent, level, attrName, attrValue) +## print("%s attr level: %d attrName |%s| attrValue: |%s|" % (indent, level, attrName, attrValue)) ## if attrValue == None: setattr(instance, attrName, attrValue) return - if type(attrValue) == types.ListType: + if type(attrValue) == list: elements = [self._unravel(element) for element in attrValue] setattr(instance, attrName, elements) return - if type(attrValue) != types.DictType: + if type(attrValue) != dict: setattr(instance, attrName, attrValue) return - # if we're here, then type(attrValue) == types.DictType + # if we're here, then type(attrValue) has to be a dict # for now, only attempt to populate fully to the third level, after that, short-circuit - if attrValue.has_key(u'_rallyAPIMajor'): - del attrValue[u'_rallyAPIMajor'] - if attrValue.has_key(u'_rallyAPIMinor'): - del attrValue[u'_rallyAPIMinor'] + if '_rallyAPIMajor' in attrValue: + del attrValue['_rallyAPIMajor'] + if '_rallyAPIMinor' in attrValue: + del attrValue['_rallyAPIMinor'] if level > 3: setattr(instance, attrName, attrValue) @@ -157,16 +151,16 @@ def _setAppropriateAttrValueForType(self, instance, attrName, attrValue, level=0 # if the attrValue contains a Count key and a _ref key and the Count is > 0, then # yank the collection ref at _ref and rename the attrName to __collection_ref_for_ - if attrValue.has_key(u'_ref') and attrValue.has_key(u'Count'): - if attrValue[u'Count'] == 0: + if '_ref' in attrValue and 'Count' in attrValue: + if attrValue['Count'] == 0: setattr(instance, attrName, []) else: - collection_ref = attrValue[u'_ref'] + collection_ref = attrValue['_ref'] setattr(instance, "__collection_ref_for_%s" % attrName, collection_ref) return if attrName == 'RevisionHistory': # this gets treated as a collection ref also at this point - collection_ref = attrValue[u'_ref'] + collection_ref = attrValue['_ref'] setattr(instance, "__collection_ref_for_%s" % attrName, collection_ref) return @@ -192,7 +186,7 @@ def _setAppropriateAttrValueForType(self, instance, attrName, attrValue, level=0 def _unravel(self, thing): - if type(thing) == types.DictType and thing.get(u'_type', None): + if type(thing) == dict and thing.get('_type', None): return self._basicInstance(thing) else: return thing diff --git a/pyral/query_builder.py b/pyral/query_builder.py index d94e37b..df26072 100644 --- a/pyral/query_builder.py +++ b/pyral/query_builder.py @@ -6,11 +6,12 @@ # ################################################################################################### -__version__ = (1, 1, 1) +__version__ = (1, 2, 0) import re import types -import urllib +import six +from six.moves.urllib.parse import quote ################################################################################################### @@ -54,15 +55,15 @@ def build(self, pretty=None): qualifiers = ['fetch=%s' % self.fetch] if self.query: ## -## print "RallyQueryFormatter raw query: %s" % self.query +## print("RallyQueryFormatter raw query: %s" % self.query) ## query_string = RallyQueryFormatter.parenGroups(self.query) ## -## print "query_string: |query=(%s)|" % query_string +## print("query_string: |query=(%s)|" % query_string) ## qualifiers.append("query=(%s)" % query_string) if self.order: - qualifiers.append("order=%s" % urllib.quote(self.order)) + qualifiers.append("order=%s" % quote(self.order)) if self.workspace: qualifiers.append(self.workspace) if self.project: @@ -80,7 +81,7 @@ def build(self, pretty=None): resource += "&".join(qualifiers) ## -## print "RallyUrlBuilder.build: resource= %s" % resource +## print("RallyUrlBuilder.build: resource= %s" % resource) ## return resource @@ -138,9 +139,9 @@ def _encode(condition): """ first_last = "%s%s" % (condition[0], condition[-1]) if first_last == "()": - url_encoded = urllib.quote(condition) + url_encoded = quote(condition) else: - url_encoded = '(%s)' % urllib.quote(condition) + url_encoded = '(%s)' % quote(condition) # replace the %xx encodings for '=', '(', ')', '!', and double quote characters readable_encoded = url_encoded.replace("%3D", '=') @@ -150,23 +151,23 @@ def _encode(condition): readable_encoded = readable_encoded.replace("%21", '!') return readable_encoded ## -## print "RallyQueryFormatter.parenGroups criteria parm: |%s|" % repr(criteria) +## print("RallyQueryFormatter.parenGroups criteria parm: |%s|" % repr(criteria)) ## - if type(criteria) in [types.ListType, types.TupleType]: + if type(criteria) in [list, tuple]: # by fiat (and until requested by a paying customer), we assume the criteria expressions are AND'ed #conditions = [_encode(expression) for expression in criteria] conditions = [expression for expression in criteria] criteria = " AND ".join(conditions) ## -## print "RallyQueryFormatter: criteria is sequence type resulting in |%s|" % criteria +## print("RallyQueryFormatter: criteria is sequence type resulting in |%s|" % criteria) ## - if type(criteria) == types.DictType: + if type(criteria) == dict: expressions = [] - for field, value in criteria.items(): + for field, value in list(criteria.items()): # have to enclose string value in double quotes, otherwise turn whatever the value is into a string - tval = '"%s"' % value if type(value) == types.StringType else '%s' % value + tval = '"%s"' % value if type(value) == bytes else '%s' % value expression = ('%s = %s' % (field, tval)) if len(criteria) == 1: return expression.replace(' ', '%20') @@ -189,15 +190,15 @@ def _encode(condition): criteria = criteria.replace('&', '%26') parts = RallyQueryFormatter.CONJUNCTION_PATT.split(criteria.strip()) ## -## print "RallyQueryFormatter parts: %s" % repr(parts) +## print("RallyQueryFormatter parts: %s" % repr(parts)) ## # if no CONJUNCTION is in parts, use the condition as is (simple case) conjunctions = [p for p in parts if p in RallyQueryFormatter.CONJUNCTIONS] if not conjunctions: - expression = urllib.quote(criteria.strip()).replace('%28', '(').replace('%29', ')') + expression = quote(criteria.strip()).replace('%28', '(').replace('%29', ')') ## -## print "RallyQueryFormatter.no_conjunctions: |%s|" % expression +## print("RallyQueryFormatter.no_conjunctions: |%s|" % expression) ## return expression @@ -209,14 +210,14 @@ def _encode(condition): conj = item binary_expression = "%s (%s)" % (conj, binary_expression) else: - cond = urllib.quote(item) + cond = quote(item) binary_expression = "(%s) %s" % (cond, binary_expression) final_expression = binary_expression.replace('%28', '(') final_expression = final_expression.replace('%29', ')') ## -## print "RallyQueryFormatter.final_expression: |%s|" % final_expression -## print "==============================================" +## print("RallyQueryFormatter.final_expression: |%s|" % final_expression) +## print("==============================================") ## final_expression = final_expression.replace(' ', '%20') return final_expression @@ -226,7 +227,9 @@ def validatePartsSyntax(parts): attr_ident = r'[\w\.]+[a-zA-Z0-9]' relationship = r'=|!=|>|<|>=|<=|contains|!contains' attr_value = r'"[^"]+"|[^" ]+' - criteria_pattern = re.compile('^(%s) (%s) (%s)$' % (attr_ident, relationship, attr_value)) + criteria_pattern = re.compile('^(%s) (%s) (%s)$' % (attr_ident, relationship, attr_value)) + quoted_value_pattern = re.compile('^(%s) (%s) ("[^"]+")$' % (attr_ident, relationship)) + unquoted_value_pattern = re.compile('^(%s) (%s) ([^"].+[^"])$' % (attr_ident, relationship)) valid_parts = [] front = "" @@ -235,6 +238,12 @@ def validatePartsSyntax(parts): mo = criteria_pattern.match(part) if mo: valid_parts.append(part) + elif quoted_value_pattern.match(part): + valid_parts.append(part) + elif unquoted_value_pattern.match(part): + wordles = part.split(' ', 2) + recast_part = '%s %s "%s"' % tuple(wordles) + valid_parts.append(recast_part) else: if re.match(r'^(AND|OR)$', part, re.I): valid_parts.append(part) @@ -242,7 +251,7 @@ def validatePartsSyntax(parts): front = part + " " if not valid_parts: - raise Exception, "Invalid query expression syntax in: %s" % (" ".join(parts)) + raise Exception("Invalid query expression syntax in: %s" % (" ".join(parts))) return valid_parts diff --git a/pyral/rallyresp.py b/pyral/rallyresp.py index 5325061..964e44c 100644 --- a/pyral/rallyresp.py +++ b/pyral/rallyresp.py @@ -10,11 +10,10 @@ # ################################################################################################### -__version__ = (1, 1, 1) +__version__ = (1, 2, 0) import sys import re -from exceptions import StopIteration import json from pprint import pprint @@ -60,7 +59,7 @@ class RallyRESTResponse(object): returned by the request (for GET). """ - def __init__(self, session, context, request, response, hydration, limit): + def __init__(self, session, context, request, response, hydration, limit, debug=False): """ A wrapper for the response received back from the REST API. The response has status_code, headers and content attributes which will be preserved. @@ -71,11 +70,12 @@ def __init__(self, session, context, request, response, hydration, limit): self.session = session self.context = context self.resource = request + self.debug = debug self.data = None request_path_elements = request.split('?')[0].split('/') ## -## print "RRR.init request: = %s " % request -## print "RRR.init request_path_elements = %s " % repr(request_path_elements) +## print("RRR.init request: = %s " % request) +## print("RRR.init request_path_elements = %s " % repr(request_path_elements)) ## self.target = request_path_elements[-1] if re.match('^\d+$', self.target): @@ -87,9 +87,9 @@ def __init__(self, session, context, request, response, hydration, limit): self.warnings = [] self._item_type = self.target ## -## print "+" * 85 -## print "resource: ", self.resource -## print "response is a %s" % type(response) +## print("+" * 85) +## print("resource: ", self.resource) +## print("response is a %s" % type(response)) ## # with attributes of status_code, content, data ## # The content is a string that needs to be turned into a json object ## # the json dict should have a key named 'QueryResult' or 'CreateResult' or @@ -98,14 +98,14 @@ def __init__(self, session, context, request, response, hydration, limit): self.status_code = response.status_code self.headers = response.headers ## -## print "RallyRESTResponse.status_code is %s" % self.status_code -## print "RallyRESTResponse.headers: %s" % repr(self.headers) +## print("RallyRESTResponse.status_code is %s" % self.status_code) +## print("RallyRESTResponse.headers: %s" % repr(self.headers)) ## # response has these keys: url, status_code, headers, raw, _content, encoding, reason, elapsed, history, connection ## ## if self.status_code == 405: -## print "RallyRESTResponse.status_code is %s" % self.status_code -## print response.content -## print "x" * 80 +## print("RallyRESTResponse.status_code is %s" % self.status_code) +## print(response.content) +## print("x" * 80) ## if isinstance(response, ErrorResponse): if 'OperationResult' in response.content: @@ -114,13 +114,17 @@ def __init__(self, session, context, request, response, hydration, limit): return self._stdFormat = True - self.content = json.loads(response.content) + try: + self.content = response.json() + except: + problem = "Response for request: {0} either was not JSON content or was an invalidly formed/incomplete JSON structure".format(self.resource) + raise RallyResponseError(problem) ## -## print "response content: %s" % self.content +## print("response content: %s" % self.content) ## self.request_type, self.data = self._determineRequestResponseType(request) ## -## print "RallyRESTResponse request_type: %s for %s" % (self.request_type, self._item_type) +## print("RallyRESTResponse request_type: %s for %s" % (self.request_type, self._item_type)) ## if self.request_type == 'ImpliedQuery': @@ -131,53 +135,53 @@ def __init__(self, session, context, request, response, hydration, limit): if target.endswith('.x'): target = target[:-2] ## -## print "ImpliedQuery presumed target: |%s|" % target -## print "" +## print("ImpliedQuery presumed target: |%s|" % target) +## print("") ## - if target not in self.content.keys(): + if target not in list(self.content.keys()): # check to see if there is a case-insensitive match before upchucking... - ckls = [k.lower() for k in self.content.keys()] + ckls = [k.lower() for k in list(self.content.keys())] if target.lower() not in ckls: forensic_info = "%s\n%s\n" % (response.status_code, response.content) problem = 'missing _Xx_Result specifier for target %s in following:' % target raise RallyResponseError('%s\n%s' % (problem, forensic_info)) else: matching_item_ix = ckls.index(target.lower()) - target = self.content.keys()[matching_item_ix] + target = list(self.content.keys())[matching_item_ix] self.target = target self._stdFormat = False # fudge in the QueryResult.Results. dict keychain self._item_type = target - self.data = {u'QueryResult': {u'Results' : { target: self.content[target] }}} - self.data[u'Errors'] = self.content[target]['Errors'] - self.data[u'Warnings'] = self.content[target]['Warnings'] + self.data = {'QueryResult': {'Results' : { target: self.content[target] }}} + self.data['Errors'] = self.content[target]['Errors'] + self.data['Warnings'] = self.content[target]['Warnings'] del self.content[target]['Errors'] # we just snagged this and repositioned it del self.content[target]['Warnings'] # ditto - self.data[u'PageSize'] = 1 - self.data[u'TotalResultCount'] = 1 + self.data['PageSize'] = 1 + self.data['TotalResultCount'] = 1 qr = self.data - self.errors = qr[u'Errors'] - self.warnings = qr[u'Warnings'] - self.startIndex = int(qr[u'StartIndex']) if u'StartIndex' in qr else 0 - self.pageSize = int(qr[u'PageSize']) if u'PageSize' in qr else 0 - self.resultCount = int(qr[u'TotalResultCount']) if u'TotalResultCount' in qr else 0 + self.errors = qr['Errors'] + self.warnings = qr['Warnings'] + self.startIndex = int(qr['StartIndex']) if 'StartIndex' in qr else 0 + self.pageSize = int(qr['PageSize']) if 'PageSize' in qr else 0 + self.resultCount = int(qr['TotalResultCount']) if 'TotalResultCount' in qr else 0 self._limit = limit if limit > 0 else self.resultCount self._page = [] - if u'Results' in qr: - self._page = qr[u'Results'] + if 'Results' in qr: + self._page = qr['Results'] else: - if u'QueryResult' in qr and u'Results' in qr[u'QueryResult']: - self._page = qr[u'QueryResult'][u'Results'] + if 'QueryResult' in qr and 'Results' in qr['QueryResult']: + self._page = qr['QueryResult']['Results'] ## -## print "initial page has %d items" % len(self._page) +## print("initial page has %d items" % len(self._page)) ## if qr.get('Object', None): self._page = qr['Object']['_ref'] ## -## print "%d items in the results starting at index: %d" % (self.resultCount, self.startIndex) +## print("%d items in the results starting at index: %d" % (self.resultCount, self.startIndex)) ## # for whatever reason, some queries where a start index is unspecified @@ -196,32 +200,32 @@ def __init__(self, session, context, request, response, hydration, limit): # transform the status code to an error code indicating an Unprocessable Entity if not already an error code self.status_code = 422 if self.status_code == 200 else self.status_code ## -## print "RallyRESTResponse, self.target: |%s|" % self.target -## print "RallyRESTResponse._page: %s" % self._page -## print "RallyRESTResponse, self.resultCount: |%s|" % self.resultCount -## print "RallyRESTResponse, self.startIndex : |%s|" % self.startIndex -## print "RallyRESTResponse, self._servable : |%s|" % self._servable -## print "" +## print("RallyRESTResponse, self.target: |%s|" % self.target) +## print("RallyRESTResponse._page: %s" % self._page) +## print("RallyRESTResponse, self.resultCount: |%s|" % self.resultCount) +## print("RallyRESTResponse, self.startIndex : |%s|" % self.startIndex) +## print("RallyRESTResponse, self._servable : |%s|" % self._servable) +## print("") ## def _determineRequestResponseType(self, request): - if u'OperationResult' in self.content: - return 'Operation', self.content[u'OperationResult'] - if u'QueryResult' in self.content: - return 'Query', self.content[u'QueryResult'] - if u'CreateResult' in self.content: - return 'Create', self.content[u'CreateResult'] - if u'UpdateResult' in self.content: - return 'Update', self.content[u'UpdateResult'] - if u'DeleteResult' in self.content: - return 'Delete', self.content[u'DeleteResult'] + if 'OperationResult' in self.content: + return 'Operation', self.content['OperationResult'] + if 'QueryResult' in self.content: + return 'Query', self.content['QueryResult'] + if 'CreateResult' in self.content: + return 'Create', self.content['CreateResult'] + if 'UpdateResult' in self.content: + return 'Update', self.content['UpdateResult'] + if 'DeleteResult' in self.content: + return 'Delete', self.content['DeleteResult'] if '_CreatedAt' in self.content and self.content['_CreatedAt'] == 'just now': return 'Create', self.content else: ## -## print "????? request type an ImpliedQuery?: %s" % request -## print self.content -## print "=" * 80 +## print("????? request type an ImpliedQuery?: %s" % request) +## print(self.content) +## print("=" * 80) ## return 'ImpliedQuery', self.content @@ -237,7 +241,7 @@ def _item(self): else: return None - def __nonzero__(self): + def __bool__(self): """ This is for evaluating any invalid response as False. """ @@ -250,6 +254,9 @@ def __iter__(self): return self def next(self): + return self.__next__() + + def __next__(self): """ Return a hydrated instance from the self.page until the page is exhausted, then issue another session.get(...request...) with startIndex @@ -257,15 +264,15 @@ def next(self): that can be manufactured (self._curIndex > self.resultCount) """ ## -## print "RallyRestResponse for %s, _stdFormat?: %s, _servable: %d _limit: %d _served: %d " % \ -## (self.target, self._stdFormat, self._servable, self._limit, self._served) +## print("RallyRestResponse for %s, _stdFormat?: %s, _servable: %d _limit: %d _served: %d " % \ +## (self.target, self._stdFormat, self._servable, self._limit, self._served)) ## if (self._served >= self._servable) or (self._limit and self._served >= self._limit): raise StopIteration if self._stdFormat: ## -## print "RallyRESTResponse.next, _stdFormat detected" +## print("RallyRESTResponse.next, _stdFormat detected") ## if self._curIndex == self.pageSize: self._page[:] = self.__retrieveNextPage() @@ -303,33 +310,57 @@ def next(self): else: # the Response had a non-std format ## ## blurb = "item from page is a %s, but Response was not in std-format" % self._item_type -## print "RallyRESTResponse.next: %s" % blurb +## print("RallyRESTResponse.next: %s" % blurb) ## # # have to stuff the item type into the item dict like it is for the _stdFormat responses # item = self._page[self._item_type] - item_type_key = u'_type' + item_type_key = '_type' if item_type_key not in item: - item[item_type_key] = unicode(self._item_type) + item[item_type_key] = str(self._item_type) + + del item['_rallyAPIMajor'] + del item['_rallyAPIMinor'] + + if self.debug: + self.showNextItem(item) - del item[u'_rallyAPIMajor'] - del item[u'_rallyAPIMinor'] -## -## print " next item served is a %s" % self._item_type -## print "RallyRESTResponse.next, item before call to to hydrator.hydrateInstance" -## for key in sorted(item.keys()): -## print " %20.20s: %s" % (key, item[key]) -## print "+ " * 30 -## entityInstance = self.hydrator.hydrateInstance(item) self._curIndex += 1 self._served += 1 ## -## print " next item served is a %s" % entityInstance._type +## print(" next item served is a %s" % entityInstance._type) ## return entityInstance + def showNextItem(item): + print(" next item served is a %s" % self._item_type) + print("RallyRESTResponse.next, item before call to to hydrator.hydrateInstance") + all_item_keys = sorted(item.keys()) + underscore_prefix_keys = [key for key in all_item_keys if key[0] == u'_'] + std_underscore_prefix_keys = ['_type', '_ref', '_refObjectUUID', '_CreatedAt', '_objectVersion'] + for _key in std_underscore_prefix_keys: + try: + print(" %20.20s: %s" % (_key, item[_key])) + except: + pass + other_prefix_keys = [key for key in underscore_prefix_keys if key not in std_underscore_prefix_keys] + for _key in other_prefix_keys: + print(" %20.20s: %s" % (_key, item[_key])) + print("") + regular_keys = [key for key in all_item_keys if key[0] !='_'] + std_regular_keys = ['ObjectID', 'ObjectUUID', 'CreationDate'] + for key in std_regular_keys: + try: + print(" %20.20s: %s" % (key, item[key])) + except: + pass + other_regular_keys = [key for key in regular_keys if key not in std_regular_keys] + for key in other_regular_keys: + print(" %20.20s: %s" % (key, item[key])) + print("+ " * 30) + def __retrieveNextPage(self): """ @@ -340,9 +371,9 @@ def __retrieveNextPage(self): if not nextPageUrl.startswith('http'): nextPageUrl = '%s/%s' % (self.context.serviceURL(), nextPageUrl) ## -## print "" -## print "full URL for next page of data:\n %s" % nextPageUrl -## print "" +## print("") +## print("full URL for next page of data:\n %s" % nextPageUrl) +## print("") ## try: response = self.session.get(nextPageUrl) @@ -353,14 +384,14 @@ def __retrieveNextPage(self): sys.exit(9) return [] - content = json.loads(response.content) - return content[u'QueryResult'][u'Results'] + content = response.json() + return content['QueryResult']['Results'] def __repr__(self): if self.status_code == 200 and self._page: try: - entity_type = self._page[0][u'_type'] + entity_type = self._page[0]['_type'] return "%s result set, totalResultSetSize: %d, startIndex: %s pageSize: %s current Index: %s" % \ (entity_type, self.resultCount, self.startIndex, self.pageSize, self._curIndex) except: diff --git a/pyral/restapi.py b/pyral/restapi.py index 288d5bb..0361ff7 100644 --- a/pyral/restapi.py +++ b/pyral/restapi.py @@ -1,21 +1,22 @@ -#!/usr/local/bin/python2.7 +#!/usr/local/bin/python3.5 ################################################################################################### # # pyral.restapi - Python Rally REST API module -# round 12 incorporating support for Rally API Key +# round 13 first stab at support for Python 3.5 or higher # notable dependencies: -# requests v2.0.0 or better +# requests v2.8.1 or better # ################################################################################################### -__version__ = (1, 1, 1) +__version__ = (1, 2, 0) import sys, os import re import types import time -import urllib +import six +from six.moves.urllib.parse import quote, unquote import json import string import base64 @@ -77,11 +78,11 @@ def getResourceByOID(context, entity, oid, **kwargs): that has status_code, headers and content attributes. """ ## -## print "getResourceByOID called:" -## print " context: %s" % context -## print " entity: %s" % entity -## print " oid: %s" % oid -## print " kwargs: %s" % kwargs +## print("getResourceByOID called:") +## print(" context: %s" % context) +## print(" entity: %s" % entity) +## print(" oid: %s" % oid) +## print(" kwargs: %s" % kwargs) ## sys.stdout.flush() ## ## if entity == 'context': @@ -93,12 +94,12 @@ def getResourceByOID(context, entity, oid, **kwargs): # raising an Exception is the only thing we can do, don't see any prospect of recovery... raise RallyRESTAPIError('Unable to find Rally instance for context: %s' % context) ## -## print "_rallyCache.keys:" +## print("_rallyCache.keys:") ## for key in _rallyCache.keys(): -## print " -->%s<--" % key -## print "" -## print " apparently no key to match: -->%s<--" % context -## print " context is a %s" % type(context) +## print(" -->%s<--" % key) +## print("") +## print(" apparently no key to match: -->%s<--" % context) +## print(" context is a %s" % type(context)) ## rally = rallyContext.get('rally') resp = rally._getResourceByOID(context, entity, oid, **kwargs) @@ -116,7 +117,7 @@ def getCollection(context, collection_url, **kwargs): global _rallyCache rallyContext = _rallyCache.get(context, None) if not rallyContext: - ck_matches = [rck for rck in _rallyCache.keys() if rck.identity() == context.identity()] + ck_matches = [rck for rck in list(_rallyCache.keys()) if rck.identity() == context.identity()] if ck_matches: rck = ck_matches.pop() rallyContext = _rallyCache[rck] @@ -171,24 +172,32 @@ class Rally(object): 'PI' : 'PortfolioItem' } FORMATTED_ID_PATTERN = re.compile(r'^[A-Z]{1,2}\d+$') #S|US|DE|DS|TA|TC|TS|PI - MAX_ATTACHMENT_SIZE = 5000000 # approx 5MB + MAX_ATTACHMENT_SIZE = 50000000 # approx 50 MB def __init__(self, server=SERVER, user=None, password=None, apikey=None, - version=WS_API_VERSION, warn=True, **kwargs): - self.server = server - self.user = user or USER_NAME - self.password = password or PASSWORD - self.apikey = apikey - self.version = WS_API_VERSION # we only support v2.0 now - self._inflated = False - self.service_url = "%s://%s/%s" % (PROTOCOL, self.server, WEB_SERVICE % self.version) - self.schema_url = "%s://%s/%s" % (PROTOCOL, self.server, SCHEMA_SERVICE % self.version) - self.hydration = "full" - self._sec_token = None - self._log = False - self._logDest = None - self._logAttrGet = False - self._warn = warn + version=WS_API_VERSION, warn=True, server_ping=None, + isolated_workspace=False, **kwargs): + self.server = server + self.user = user or USER_NAME + self.password = password or PASSWORD + self.apikey = apikey + self.version = WS_API_VERSION # we only support v2.0 now + self._inflated = False + self.service_url = "%s://%s/%s" % (PROTOCOL, self.server, WEB_SERVICE % self.version) + self.schema_url = "%s://%s/%s" % (PROTOCOL, self.server, SCHEMA_SERVICE % self.version) + self.hydration = "full" + self._sec_token = None + self._log = False + self._logDest = None + self._logAttrGet = False + self._warn = warn + self._server_ping = True # this is the default for 1.2.0 + if 'RALLY_PING' in os.environ: + if os.environ['RALLY_PING'].lower() in ['f', 'false', 'n', 'no', '0']: + self._server_ping = False + if server_ping == False: + self._server_ping = False + self.isolated_workspace = isolated_workspace config = {} if kwargs and 'debug' in kwargs and kwargs.get('debug', False): config['verbose'] = sys.stdout @@ -207,7 +216,9 @@ def __init__(self, server=SERVER, user=None, password=None, apikey=None, vsc = kwargs.get('verify_ssl_cert') if vsc in [False, True]: verify_ssl_cert = vsc - +## +## print("\n requests lib in %s" % requests.__file__) +## self.session = requests.Session() self.session.headers = RALLY_REST_HEADERS if self.apikey: @@ -223,9 +234,16 @@ def __init__(self, server=SERVER, user=None, password=None, apikey=None, global _rallyCache - self.contextHelper = RallyContextHelper(self, self.server, self.user, self.password or self.apikey) + self.contextHelper = RallyContextHelper(self, self.server, self.user, self.password or self.apikey, + self._server_ping) _rallyCache[self.contextHelper.context] = {'rally' : self } - self.contextHelper.check(self.server) + wksp = None + proj = None + if 'workspace' in kwargs and kwargs['workspace'] and kwargs['workspace']!= 'default': + wksp = kwargs['workspace'] + if 'project' in kwargs and kwargs['project'] and kwargs['project']!= 'default': + proj = kwargs['project'] + self.contextHelper.check(self.server, wksp, proj, self.isolated_workspace) if self.contextHelper.currentContext() not in _rallyCache: _rallyCache[self.contextHelper.currentContext()] = {'rally' : self} @@ -303,8 +321,8 @@ def obtainSecurityToken(self): if not self._sec_token: security_service_url = "%s/%s" % (self.service_url, AUTH_ENDPOINT) response = self.session.get(security_service_url) - doc = json.loads(response.content) - self._sec_token = str(doc[u'OperationResult'][u'SecurityToken']) + doc = response.json() + self._sec_token = str(doc['OperationResult']['SecurityToken']) return self._sec_token @@ -319,7 +337,7 @@ def enableLogging(self, dest=sys.stdout, attrget=False, append=False): self._log = True if hasattr(dest, 'write'): self._logDest = dest - elif type(dest) == types.StringType: + elif type(dest) == bytes: try: mode = 'w' if append: @@ -376,7 +394,12 @@ def setWorkspace(self, workspaceName): """ Given a workspaceName, set that as the currentWorkspace and use the ref for that workspace in subsequent interactions with Rally. + However, if the instance was realized using the keyword arg isolated_workspace = True + then do not permit the workspace switch to take place, raise an Exception. """ + if self.isolated_workspace and workspaceName != self.getWorkspace().Name: + problem = "No reset of of the Workspace is permitted when the isolated_workspace option is specified" + raise RallyRESTAPIError(problem) if not self.contextHelper.isAccessibleWorkspaceName(workspaceName): raise Exception('Specified workspace not valid for your credentials or is in a Closed state') self.contextHelper.setWorkspace(workspaceName) @@ -494,7 +517,7 @@ def getUserInfo(self, oid=None, username=None, name=None): Returns either a single User instance or a list of User instances """ - context = self.contextHelper.currentContext() + #context = self.contextHelper.currentContext() item, response = None, None if oid: item = self._itemQuery('User', oid) @@ -532,6 +555,9 @@ def getAllUsers(self, workspace=None): # Somewhere post 1.3x in Rally WSAPI, the ability to list the User attrs along with the TimeZone # attr of UserProfile and have that all returned in 1 query was no longer supported. + # And somewhere north of v 1.42 and into v2, only a user who is a SubscriptionAdmin + # can actually get information about another user's UserProfile so the next statement + # is limited to a Rally instance whose credentials represent a SubscriptionAdmin capable user. # So we do a full bucket query on User and UserProfile separately and "join" them via our # own brute force method so that the the caller can access any UserProfile attribute # for a User. @@ -547,7 +573,7 @@ def getAllUsers(self, workspace=None): # and other UserProfile attributes ] - users_resource = 'users?fetch=true&query=&pagesize=%s&start=1&workspace=%s' % (MAX_PAGESIZE, workspace_ref) + users_resource = 'users?fetch=%s&query=&pagesize=%s&start=1&workspace=%s' % (",".join(user_attrs), MAX_PAGESIZE, workspace_ref) full_resource_url = '%s/%s' % (self.service_url, users_resource) response = self.session.get(full_resource_url, timeout=SERVICE_REQUEST_TIMEOUT) if response.status_code != HTTP_REQUEST_SUCCESS_CODE: @@ -555,6 +581,12 @@ def getAllUsers(self, workspace=None): response = RallyRESTResponse(self.session, context, users_resource, response, "full", 0) users = [user for user in response] + # find the operator of this instance of Rally and short-circuit now if they *aren't* a SubscriptionAdmin + operator = [user for user in users if user.UserName == self.user] + if not operator or len(operator) == 0 or not operator[0].SubscriptionAdmin: + self.setWorkspace(saved_workspace_name) + return users + user_profile_resource = 'userprofile?fetch=true&query=&pagesize=%s&start=1&workspace=%s' % (MAX_PAGESIZE, workspace_ref) response = self.session.get('%s/%s' % (self.service_url, user_profile_resource), timeout=SERVICE_REQUEST_TIMEOUT) @@ -571,8 +603,9 @@ def getAllUsers(self, workspace=None): mups = [prof for prof in profiles if hasattr(user, 'UserProfile') and prof._ref == user.UserProfile._ref] if not mups: + up = user.UserProfile if hasattr(user, 'UserProfile') else "Unknown" problem = "unable to find a matching UserProfile record for User: %s UserProfile: %s" - warning("%s" % (problem % (user.DisplayName, user.UserProfile))) + #warning("%s" % (problem % (user.UserName, up))) continue else: if len(mups) > 1: @@ -609,21 +642,21 @@ def _getResourceByOID(self, context, entity, oid, **kwargs): Returns a raw response instance (with status_code, headers and content attributes). """ ## -## print "in _getResourceByOID, OID specific resource ...", entity, oid +## print("in _getResourceByOID, OID specific resource ...", entity, oid) ## sys.stdout.flush() ## resource = '%s/%s' % (entity, oid) if '_disableAugments' not in kwargs: contextDict = context.asDict() ## -## print "_getResourceByOID, current contextDict: %s" % repr(contextDict) +## print("_getResourceByOID, current contextDict: %s" % repr(contextDict)) ## sys.stdout.flush() ## context, augments = self.contextHelper.identifyContext(**contextDict) if augments: resource += ("?" + "&".join(augments)) ## -## print "_getResourceByOID, modified contextDict: %s" % repr(context.asDict()) +## print("_getResourceByOID, modified contextDict: %s" % repr(context.asDict())) ## sys.stdout.flush() ## full_resource_url = "%s/%s" % (self.service_url, resource) @@ -631,7 +664,7 @@ def _getResourceByOID(self, context, entity, oid, **kwargs): self._logDest.write('%s GET %s\n' % (timestamp(), resource)) self._logDest.flush() ## -## print "issuing GET for resource: %s" % full_resource_url +## print("issuing GET for resource: %s" % full_resource_url) ## sys.stdout.flush() ## try: @@ -641,7 +674,7 @@ def _getResourceByOID(self, context, entity, oid, **kwargs): warning("%s: %s" % (exctype, value)) return None ## -## print "_getResourceByOID(%s, %s) raw_response: %s" % (entity, oid, raw_response) +## print("_getResourceByOID(%s, %s) raw_response: %s" % (entity, oid, raw_response)) ## sys.stdout.flush() ## return raw_response @@ -652,7 +685,7 @@ def _itemQuery(self, entityName, oid, workspace=None, project=None): Internal method to retrieve a specific instance of an entity identified by the OID. """ ## -## print "Rally._itemQuery('%s', %s, workspace=%s, project=%s)" % (entityName, oid, workspace, project) +## print("Rally._itemQuery('%s', %s, workspace=%s, project=%s)" % (entityName, oid, workspace, project)) ## resource = '%s/%s' % (entityName, oid) context, augments = self.contextHelper.identifyContext(workspace=workspace, project=project) @@ -699,19 +732,19 @@ def _greased(self, item_data): Return the item_dict with any updates to COLLECTIONS attributes that needed "greasing". """ - collection_attributes = [attr_name for attr_name in item_data.keys() + collection_attributes = [attr_name for attr_name in list(item_data.keys()) if attr_name.lower == 'children' or attr_name[-1] == 's' ] if not collection_attributes: return item_data for attr_name in collection_attributes: - if type(item_data[attr_name]) != types.ListType: + if type(item_data[attr_name]) != list: continue obj_list = [] for value in item_data[attr_name]: # is value like "someentityname/34223214" ? - if type(value) == types.StringType and '/' in value \ + if type(value) == bytes and '/' in value \ and re.match('^\d+$', value.split('/')[-1]): obj_list.append({"_ref" : value}) # transform to a dict instance else: @@ -748,11 +781,11 @@ def _buildRequest(self, entity, fetch, query, order, kwargs): elif fetch in ['false', 'False', False]: fetch = 'false' self.hydration = "shell" - elif type(fetch) == types.StringType and fetch.lower() != 'false': + elif type(fetch) == bytes and fetch.lower() != 'false': self.hydration = "full" - elif type(fetch) in [types.ListType, types.TupleType]: + elif type(fetch) in [list, tuple]: attr_info = self.validateAttributeNames(entity, dict([(attr_name,True) for attr_name in fetch])) - fetch = ",".join(k for k in attr_info.keys()) + fetch = ",".join(k for k in list(attr_info.keys())) self.hydration = "full" entity = self._officialRallyEntityName(entity) @@ -789,7 +822,7 @@ def _getRequestResponse(self, context, request_url, limit): except Exception as ex: if response: ## -## print "Exception detected for session.get requests, response status code: %s" % response.status_code +## print("Exception detected for session.get requests, response status code: %s" % response.status_code) ## ret_code, content = response.status_code, response.content else: @@ -803,7 +836,7 @@ def _getRequestResponse(self, context, request_url, limit): return response ## -## print "response.status_code is %s" % response.status_code +## print("response.status_code is %s" % response.status_code) ## if response.status_code != HTTP_REQUEST_SUCCESS_CODE: if self._log: @@ -811,7 +844,7 @@ def _getRequestResponse(self, context, request_url, limit): self._logDest.write('%s %s %s ...\n' % (timestamp(), code, verbiage)) self._logDest.flush() ## -## print response +## print(response) ## #if response.status_code == PAGE_NOT_FOUND_CODE: # problem = "%s Service unavailable from %s, check for proper hostname" % \ @@ -869,8 +902,8 @@ def get(self, entity, fetch=False, query=None, order=None, **kwargs): context, resource, full_resource_url, limit = self._buildRequest(entity, fetch, query, order, kwargs) if self._log: - #urllib.unquote the resource for enhanced readability - self._logDest.write('%s GET %s\n' % (timestamp(), urllib.unquote(resource))) + # unquote the resource for enhanced readability + self._logDest.write('%s GET %s\n' % (timestamp(), unquote(resource))) self._logDest.flush() response = self._getRequestResponse(context, full_resource_url, limit) @@ -890,10 +923,9 @@ def getCollection(self, collection_url, **kwargs): in the collection. """ context = self.contextHelper.currentContext() - collection_url = "%s?pagesize=%d&start=1" % (collection_url, MAX_PAGESIZE) -## -## print "Collection URL: %s" % collection_url -## + # craven ugly hackiness... + if not '?fetch=' in collection_url: + collection_url = "%s?pagesize=%d&start=1" % (collection_url, MAX_PAGESIZE) resource = collection_url disabled_augments = kwargs.get('_disableAugments', False) @@ -902,7 +934,7 @@ def getCollection(self, collection_url, **kwargs): project_ref = self.contextHelper.currentProjectRef() resource = "%s&workspace=%s&project=%s" % (resource, workspace_ref, project_ref) ## -## print "Collection resource URL: %s" % resource +## print("Collection resource URL: %s" % resource) ## if self._log: self._logDest.write('%s GET %s\n' % (timestamp(), resource)) @@ -946,14 +978,16 @@ def put(self, entityName, itemData, workspace='current', project='current', **kw response = self.session.put(full_resource_url, data=payload, headers=RALLY_REST_HEADERS) response = RallyRESTResponse(self.session, context, resource, response, "shell", 0) if response.status_code != HTTP_REQUEST_SUCCESS_CODE: - desc = response.errors[0] + desc = str(response.errors[0]) + problem = "%s %s" % (response.status_code, desc) + #print(problem) if self._log: - self._logDest.write('%s %s %s\n' % (timestamp(), response.status_code, desc)) + self._logDest.write('%s %s\n' % (timestamp(), problem)) self._logDest.flush() - raise RallyRESTAPIError('%s %s' % (response.status_code, desc)) + raise RallyRESTAPIError(problem) - item = response.content[u'CreateResult'][u'Object'] - ref = str(item[u'_ref']) + item = response.content['CreateResult']['Object'] + ref = str(item['_ref']) item_oid = int(ref.split('/')[-1]) desc = "created %s OID: %s" % (entityName, item_oid) if self._log: @@ -982,7 +1016,7 @@ def post(self, entityName, itemData, workspace='current', project='current', **k entityName = self._officialRallyEntityName(entityName) if entityName.lower() == 'recyclebinentry': - raise RallyRESTAPIError("create operation unsupported for RecycleBinEntry") + raise RallyRESTAPIError("update operation unsupported for RecycleBinEntry") oid = itemData.get('ObjectID', None) if not oid: @@ -997,19 +1031,14 @@ def post(self, entityName, itemData, workspace='current', project='current', **k target = response.next() oid = target.ObjectID -## -## print "target OID: %s" % oid -## itemData['ObjectID'] = oid + resource = '%s/%s?key=%s' % (entityName.lower(), oid, auth_token) context, augments = self.contextHelper.identifyContext(workspace=workspace, project=project) if augments: resource += ("&" + "&".join(augments)) full_resource_url = "%s/%s" % (self.service_url, resource) -## -## print "resource: %s" % resource -## itemData = self.validateAttributeNames(entityName, itemData) item = {entityName: self._greased(itemData)} payload = json.dumps(item) @@ -1047,7 +1076,7 @@ def delete(self, entityName, itemIdent, workspace='current', project='current', # guess at whether itemIdent is an ObjectID or FormattedID via # regex matching (all digits or 1-2 upcase chars + digits) objectID = itemIdent # at first assume itemIdent is the ObjectID - if re.match('^[A-Z]{1,2}\d+$', itemIdent): + if re.match('^[A-Z]{1,2}\d+$', str(itemIdent)): fmtIdQuery = 'FormattedID = "%s"' % itemIdent response = self.get(entityName, fetch="ObjectID", query=fmtIdQuery, workspace=workspace, project=project) @@ -1058,7 +1087,7 @@ def delete(self, entityName, itemIdent, workspace='current', project='current', objectID = target.ObjectID ## ## if kwargs.get('debug', False): -## print "DEBUG: target OID -> %s" % objectID +## print("DEBUG: target OID -> %s" % objectID) ## resource = "%s/%s?key=%s" % (entityName.lower(), objectID, auth_token) context, augments = self.contextHelper.identifyContext(workspace=workspace, project=project) @@ -1075,7 +1104,7 @@ def delete(self, entityName, itemIdent, workspace='current', project='current', self._logDest.flush() ## ## if kwargs.get('debug', False): -## print response.status_code, response.headers, response.content +## print(response.status_code, response.headers, response.content) ## errorResponse = ErrorResponse(response.status_code, response.content) response = RallyRESTResponse(self.session, context, resource, errorResponse, self.hydration, 0) @@ -1084,7 +1113,7 @@ def delete(self, entityName, itemIdent, workspace='current', project='current', raise RallyRESTAPIError(problem) ## -## print response.content +## print(response.content) ## response = RallyRESTResponse(self.session, context, resource, response, "shell", 0) if response.errors: @@ -1142,9 +1171,9 @@ def search(self, keywords, **kwargs): kwargs['_slug'] = "/search" kwargs['pagesize'] = 200 kwargs['searchScope'] = 'project' - if not kwargs.has_key('projectScopeUp'): + if 'projectScopeUp' not in kwargs: kwargs['projectScopeUp'] = False - if not kwargs.has_key('projectScopeDown'): + if 'projectScopeDown' not in kwargs: kwargs['projectScopeDown'] = False # unfortunately, the WSAPI seems to not recognize/operate on projectScopeX, searchScopeX parameters... @@ -1167,16 +1196,16 @@ def search(self, keywords, **kwargs): #resource_url = "%s&pagesize=%s" % (left, right) url, query_string = resource_url.split('?', 1) - resource_url = "%s?keywords=%s&%s" % (url, urllib.quote(keywords), query_string) + resource_url = "%s?keywords=%s&%s" % (url, quote(keywords), query_string) ## - print resource_url +## print(resource_url) ## response = self._getRequestResponse(context, resource_url, limit) if response.errors: error_text = response.errors[0] raise RallyRESTAPIError(error_text) ## - print response.data +## print(response.data) ## # since the WSAPI apparently doesn't pay attention to scoping (projectScopeUp, projectScopeDown, searchScopeUp, searchScopeDown) @@ -1244,8 +1273,8 @@ def getSchemaInfo(self, workspace, project=None): # Rally has to go pull the information again which could take somewhat longer. # We don't use it here as we don't account for the potential of a _really_ long winded process during which # Rally schema changes may be made. - #print response.content - return json.loads(response.content)[u'QueryResult'][u'Results'] + #print(response.content) + return response.json()['QueryResult']['Results'] def typedef(self, target_type): @@ -1274,17 +1303,17 @@ def validateAttributeNames(self, entity_name, itemData): entity_def = self.typedef(entity_name) entity_attributes = entity_def.Attributes ## -## print "%s attributes:" +## print("%s attributes:") ## for attr in entity_attributes: -## print " |%s|" % attr.ElementName -## print "" +## print(" |%s|" % attr.ElementName) +## print("") ## attr_forms = [(attr.ElementName, attr.ElementName.lower(), attr.Name.lower().replace(' ', '')) for attr in entity_attributes] # ElementName, lower case ElementName, lower case Name txfmed_item_data = {} invalid_attrs = [] - for item_attr_name, item_attr_value in itemData.items(): + for item_attr_name, item_attr_value in list(itemData.items()): eln_hits = [eln for eln, ell, anl in attr_forms if item_attr_name == eln] if eln_hits: # is the item_attr_name an exact match for an Attribute.ElementName ? txfmed_item_data[item_attr_name] = item_attr_value @@ -1353,23 +1382,32 @@ def getStates(self, entity): state_ix = {} for state in [item for item in response]: state_ix[(state.OrderIndex, state.Name)] = state - state_keys = sorted(state_ix.keys(), key=itemgetter(0)) + state_keys = sorted(list(state_ix.keys()), key=itemgetter(0)) states = [state_ix[key] for key in state_keys] return states def getAllowedValues(self, entityName, attributeName, **kwargs): +## +## print("%s attribute name: %s" % (entityName, attributeName)) +## schema_item = self.contextHelper.getSchemaItem(entityName) if not schema_item: # TODO: should we actually raise an exception here? return None if not schema_item.completed: +## +## print("the schema_item was NOT completed, calling the complete method") +## schema_item.complete(self.contextHelper.currentContext(), getCollection) matching_attrs = [attr for attr in schema_item.Attributes if attr.ElementName == attributeName] if not matching_attrs: return None attribute = matching_attrs[0] +## +## print(" AllowedValues: %s" % (attribute.AllowedValues)) +## # suggested by Scott Vitale to address issue in Rally WebServices response # (sometimes value is present, other times StringValue must be used) return [av.StringValue for av in attribute.AllowedValues] @@ -1421,8 +1459,12 @@ def addAttachment(self, artifact, filename, mime_type='text/plain'): return already_attached[0] contents = '' - with open(filename, 'r') as af: + with open(filename, 'rb') as af: contents = base64.b64encode(af.read()) + # In Python 3.x, contents comes back as bytes, in order for json/encoder to be able + # to do its job, we have to get the repr of contents (eg, b'VGldfak890b325bh') + # and strip off the bytes quoting characters leaving value VGldfak890b325bh + if six.PY3: contents = repr(contents)[2:-1] # create an AttachmentContent item ac = self.create('AttachmentContent', {"Content" : contents}, project=None) @@ -1469,7 +1511,7 @@ def addAttachments(self, artifact, attachments): ct_item = attachment.get('mime_type', None) or attachment.get('MimeType', None) \ or attachment.get('content_type', None) or attachment.get('ContentType', None) if not ct_item: - print "Bypassing attachment for %s, no mime_type/ContentType setting..." % att_name + print("Bypassing attachment for %s, no mime_type/ContentType setting..." % att_name) continue candidates.append(att_name) upd_artifact = self.addAttachment(artifact, att_name, mime_type=ct_item) @@ -1555,18 +1597,15 @@ def deleteAttachment(self, artifact, filename): # get the target Attachment and the associated AttachmentContent item attachment = hits.pop(0) -## -## print attachment.details() -## if attachment.Content and attachment.Content.oid: success = self.delete('AttachmentContent', attachment.Content.oid, project=None) if not success: - print "ERROR: Unable to delete AttachmentContent item for %s" % attachment.Name + print("ERROR: Unable to delete AttachmentContent item for %s" % attachment.Name) return False deleted = self.delete('Attachment', attachment.oid, project=None) if not deleted: - print "ERROR: Unable to delete Attachment for %s" % attachment.Name + print("ERROR: Unable to delete Attachment for %s" % attachment.Name) return False remaining_attachments = [att for att in current_attachments if att.ref != attachment.ref] att_refs = [dict(_ref=str(att.ref)) for att in remaining_attachments] @@ -1605,9 +1644,107 @@ def _realizeArtifact(self, artifact): artifact = response.next() else: art_type = False - else: # artifact isn't anything we can deal with here... + else: # the supplied artifact isn't anything we can deal with here... pass return art_type, artifact + + + def rankAbove(self, reference_artifact, target_artifact): + """ + Given a reference_artifact and a target_artifact, make a Rally WSAPI PUT call + to ...//target_artifact.oid?rankAbove=reference_artifact.ref + """ + return self._rankRelative(reference_artifact, target_artifact, 'Above') + + + def rankBelow(self, reference_artifact, target_artifact): + """ + Given a reference_artifact and a target_artifact, make a Rally WSAPI PUT call + to ...//target_artifact.oid?rankBelow=reference_artifact.ref + """ + return self._rankRelative(reference_artifact, target_artifact, 'Below') + + + def rankToTop(self, target_artifact): + """ + Given a target_artifact, make a Rally WSAPI PUT call + to ...//target_artifact.oid?rankTo=TOP + """ + return self._rankTo(target_artifact, 'TOP') + + def rankToBottom(self, target_artifact): + """ + Given a target_artifact, make a Rally WSAPI PUT call + to ...//target_artifact.oid?rankTo=BOTTOM + """ + return self._rankTo(target_artifact, 'BOTTOM') + + + def _rankRelative(self, reference_artifact, target_artifact, direction): + """ + Given a reference_artifact and target_artifact, make a Rally WSAPI POST call + to ...//target_artifact.oid?rankXxx=reference_artifact.oid&key=xx&workspace=yyy. + The POST also must include post data of {artifact_type:{'_ref':target_artifact.ref}} + in spite of the fact the target artifact's oid is already part of the resource URI. Wot??... + """ + artifact_type = self._ensureRankItemSanity(target_artifact) + resource = '%s/%s?&rank%s=%s' % (artifact_type, target_artifact.oid, direction, reference_artifact.ref) + update_item = {artifact_type:{'_ref':target_artifact.ref}} + return self._postRankRequest(target_artifact, resource, update_item) + + def _rankTo(self, target_artifact, location): + """ + Given a reference_artifact, make a Rally WSAPI POST call + to ...//target_artifact.oid?rankTo=TOP|BOTTOM&key=xx&workspace=yyy. + The POST also must include post data of {artifact_type:{'_ref':target_artifact.ref}} + in spite of the fact the target artifact's oid is already part of the resource URI. Double wot??... + """ + artifact_type = self._ensureRankItemSanity(target_artifact) + resource = '%s/%s?&rankTo=%s' % (artifact_type, target_artifact.oid, location) + update_item = {artifact_type:{'_ref':target_artifact.ref}} + return self._postRankRequest(target_artifact, resource, update_item) + + def _postRankRequest(self, target_artifact, resource, update_item): + """ + Given an AgileCentral target Artifact and a resource URI (sans the self.service_url prefix) + and a dict that serves as a "container" for the target item's _ref value, + obtain the security token we need to post to AgileCentral, construct the + full url along with the query string containing the workspace ref and the security token. + POST to the resource supplying the update_item "container" and catch any + non success status code returned from the operation. + If the POST and parsing of the response was succesful, return the response instance. + """ + workspace_ref = self.contextHelper.currentWorkspaceRef() + auth_token = self.obtainSecurityToken() + full_resource_url = "%s/%s&workspace=%s&key=%s" % (self.service_url, resource, workspace_ref, auth_token) + payload = json.dumps(update_item) + response = self.session.post(full_resource_url, data=payload, headers=RALLY_REST_HEADERS) + context = self.contextHelper.currentContext() + response = RallyRESTResponse(self.session, context, resource, response, "shell", 0) + if response.status_code != HTTP_REQUEST_SUCCESS_CODE: + problem = 'Unable to update the DragAndDropRank value for the target_artifact %s, %s' + raise RallyRESTAPIError(problem % (target_artifact.FormattedID, response.errors[0])) + return response + + def _ensureRankItemSanity(self, target_artifact, reference_artifact=None): + """ + Ranking can only be done for an item that is an Artifact subclass. + If a reference_artifact is supplied, it too must be an Artifact subclass instance. + """ + class_ancestors = [cls.__name__ for cls in target_artifact.__class__.mro()] + target_is_artifact = 'Artifact' in class_ancestors + if not target_is_artifact: + problem = "Unable to change DragAndDropRank for target %s, not an Artifact" + raise RallyRESTAPIError(problem % (target_artifact.__class__.__name__)) + if reference_artifact: + class_ancestors = [cls.__name__ for cls in reference_artifact.__class__.mro()] + reference_is_artifact = 'Artifact' in class_ancestors + if not reference_is_artifact: + problem = "Unable to change DragAndDropRank for target %s, reference item is not an Artifact" + raise RallyRESTAPIError(problem % (target_artifact.__class__.__name__)) + + return target_artifact.__class__.__name__.lower() + ################################################################################################## diff --git a/pyral/search_utils.py b/pyral/search_utils.py index b960481..9e78715 100644 --- a/pyral/search_utils.py +++ b/pyral/search_utils.py @@ -1,5 +1,7 @@ from operator import attrgetter +import six + def projectAncestors(target_project, project_pool, ancestors): if target_project.Parent: ancestors.append(target_project.Parent) @@ -26,7 +28,7 @@ def flatten(target_dict, sort_attr, list_o_things): keys to be in a flat list of values. """ - for key in sorted(target_dict.keys(), key=attrgetter(sort_attr)): + for key in sorted(list(target_dict.keys()), key=attrgetter(sort_attr)): list_o_things.append(getattr(key, sort_attr)) value = target_dict[key] if isinstance(value, dict): @@ -56,6 +58,6 @@ def __init__(self, items): def __iter__(self): return self.items - def next(self): - return self.items.next() + def __next__(self): + return six.next(self.items) diff --git a/rallyfire.py b/rallyfire.py index 8517727..28f96d7 100755 --- a/rallyfire.py +++ b/rallyfire.py @@ -21,7 +21,7 @@ def main(args): options = [opt for opt in args if opt.startswith('--')] args = [arg for arg in args if arg not in options] server, user, password, apikey, workspace, project = rallyWorkset(options) - print " ".join(["|%s|" % item for item in [server, user, password, workspace, project]]) + print(" ".join(["|%s|" % item for item in [server, user, password, workspace, project]])) # If you want to use BasicAuth, use the following form rally = Rally(server, user, password, workspace=workspace, project=project) # If you want to use API Key, you can use the following form @@ -34,28 +34,28 @@ def main(args): specified_workspace = workspace workspace = rally.getWorkspace() - print "Workspace: %s " % workspace.Name + print("Workspace: %s " % workspace.Name) if specified_workspace != workspace.Name: - print " ** The workspace you specified: %s is not a valid workspace name for your account, using your default workspace instead" % specified_workspace + print(" ** The workspace you specified: %s is not a valid workspace name for your account, using your default workspace instead" % specified_workspace) #print "Workspace: %12.12s %-18.18s (%s)" % (workspace.oid, workspace.Name, workspace.ref) project = rally.getProject() - print "Project : %s " % project.Name - #print "Project : %12.12s %-18.18s (%s)" % (project.oid, project.Name, project.ref) + print("Project : %s " % project.Name) + #print("Project : %12.12s %-18.18s (%s)" % (project.oid, project.Name, project.ref)) # uncomment this to see all of your accessible workspaces and projects # workspaces = rally.getWorkspaces() # for workspace in workspaces: -# print " ", workspace.Name +# print(" ", workspace.Name) # projects = rally.getProjects(workspace=workspace.Name) # if projects: -# print "" -# print " Projects:" +# print("") +# print(" Projects:") # for project in projects: -# print " ", project.Name +# print(" ", project.Name) # else: -# print " No projects" -# print "" +# print(" No projects") +# print("") sys.exit(0) diff --git a/setup.py b/setup.py index 4f6fa86..5b064d6 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from setuptools import setup PACKAGE = 'pyral' -VERSION = '1.1.1' +VERSION = '1.2.0' OFFICIAL_NAME = 'Python toolkit for Rally REST API' PKG_URL_NAME = 'python-toolkit-rally-rest-api' AUTHOR = 'Kip Lehman (Rally Software Development)' @@ -21,7 +21,7 @@ GITHUB_DISTS = '%s/blob/master/dists' % GITHUB_SITE DOWNLOADABLE_ZIP = '%s/%s-%s.zip' % (GITHUB_DISTS, PACKAGE, VERSION) -MINIMUM_REQUESTS_VERSION = '2.0.0' +MINIMUM_REQUESTS_VERSION = '2.3.0' setup(name=PACKAGE, version=VERSION, @@ -33,7 +33,7 @@ long_description=open('README.rst').read(), packages=[PACKAGE], license='BSD', - requires=["python (< 3.0)"], + #requires=["python"], #install_requires=['requests>=%s' % MINIMUM_REQUESTS_VERSION], classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -44,6 +44,7 @@ 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.5', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Software Development :: Libraries', ], diff --git a/test/test_attachments.py b/test/test_attachments.py index 4745c51..b2c7bf9 100644 --- a/test/test_attachments.py +++ b/test/test_attachments.py @@ -56,10 +56,10 @@ def test_add_attachment(): #for story in response: # print "%s %-48.48s %d" % (story.FormattedID, story.Name, len(story.Attachments)) - candidate_story = "US96" + candidate_story = "US1" # was "US96" in trial response = rally.get("UserStory", fetch="FormattedID,Name,Attachments", query='FormattedID = "%s"' % candidate_story) - print response.resultCount + ##print(response.resultCount) story = response.next() assert len(story.Attachments) == 0 @@ -80,7 +80,7 @@ def test_get_attachment(): """ """ rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) - candidate_story = "US80" + candidate_story = "US2" # was this in trial -> "US80" target = 'FormattedID = "%s"' % candidate_story response = rally.get("UserStory", fetch=True, query=target, project=None) assert response.resultCount == 1 @@ -115,8 +115,8 @@ def test_add_tcr_attachment(): response = rally.get('Project', fetch=False, limit=10) assert response != None assert response.status_code == 200 - proj = rally.getProject() # proj.Name == My Project - assert proj.Name == 'My Project' + proj = rally.getProject() # proj.Name == Sample Project + assert proj.Name == 'Sample Project' tc_info = { "Workspace" : wksp.ref, "Project" : proj.ref, @@ -124,16 +124,16 @@ def test_add_tcr_attachment(): "Type" : "Functional", } test_case = rally.create('TestCase', tc_info) - assert test_case.oid > 0 + assert int(test_case.oid) > 0 tcr_info = { "Workspace" : wksp.ref, "TestCase" : test_case.ref, - "Date" : "2014-05-17T14:30:28.000Z", - "Build" : 27, + "Date" : "2016-05-17T14:30:28.000Z", + "Build" : 17, "Verdict" : "Pass" } tcr = rally.create('TestCaseResult', tcr_info) - assert tcr.oid > 0 + assert int(tcr.oid) > 0 attachment_name = "Addendum.txt" att_ok = conjureUpAttachmentFile(attachment_name) @@ -148,7 +148,7 @@ def test_detach_attachment(): This is the counterpart test for test_add_attachment """ rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) - candidate_story = "US96" + candidate_story = "US1" # "US96" target = 'FormattedID = "%s"' % candidate_story response = rally.get("UserStory", fetch=True, query=target, project=None) diff --git a/test/test_conn.py b/test/test_conn.py index 619484d..ba1bfe3 100644 --- a/test/test_conn.py +++ b/test/test_conn.py @@ -4,11 +4,13 @@ import types import py import time +import re import pyral from pyral import Rally -RallyRESTAPIError = pyral.context.RallyRESTAPIError +RallyRESTAPIError = pyral.context.RallyRESTAPIError +RallyResponseError = pyral.rallyresp.RallyResponseError ################################################################################################## @@ -46,6 +48,27 @@ def test_basic_connection(): # del os.environ['https_proxy'] # time.sleep(1) +#def test_connection_proxy_with_api_key(): +# """ +# Using a known valid Rally server and access credentials, issue a simple query +# request against a known valid Rally entity via use of https_proxy. +# """ +# os.environ['https_proxy'] = "http://%s" % HTTPS_PROXY +# +# rally = Rally(server=TRIAL, apikey=API_KEY, server_ping=False) +# rally.setWorkspace('Rally') +# projects = rally.getProjects() +# project_names = sorted([proj.Name for proj in projects]) +# AWESOME_PROJECT = 'Alligator Tiers' +# assert AWESOME_PROJECT in project_names +# response = rally.get('Project', fetch=False, limit=10) +# assert response != None +# assert response.status_code == 200 +# rally.setProject(AWESOME_PROJECT) +# project = rally.getProject() +# assert project.Name == AWESOME_PROJECT + + def test_basic_connection_with_apikey(): """ Using a known valid Rally server and valid API Key value, @@ -148,28 +171,29 @@ def test_non_rally_server(): Do the same test using default access credentials and known correct valid credentials to an existing Rally server. The attempt must generate an Exception - The status_code in the response must indicate a non-success condition. """ non_rally_server = 'www.irs.gov' - expectedErrMsg = "404 Target host: '%s' doesn't support the Rally WSAPI" % non_rally_server - timeoutMsg = "Request timed out on attempt to reach %s" % non_rally_server - with py.test.raises(RallyRESTAPIError) as excinfo: + #expectedErrMsg = "404 Target host: '%s' doesn't support the Rally WSAPI" % non_rally_server + #timeoutMsg = "Request timed out on attempt to reach %s" % non_rally_server + #expectedErrMsg = "Response for request: .+//%s/.+ either was not JSON content or was an invalidly formed" + expectedErrMsg = "Response for request: .*%s.* either was not JSON content or was an invalidly formed\/incomplete JSON structure" % non_rally_server + with py.test.raises(RallyResponseError) as excinfo: rally = Rally(server=non_rally_server, timeout=5) actualErrVerbiage = excinfo.value.args[0] # becuz Python2.6 deprecates message :-( - #print " expectedErrMsg: %s" % expectedErrMsg - #print " actualErrVerbiage: %s" % actualErrVerbiage - assert excinfo.value.__class__.__name__ == 'RallyRESTAPIError' - assert (actualErrVerbiage == expectedErrMsg or actualErrVerbiage == timeoutMsg) + #print(" expectedErrMsg: %s" % expectedErrMsg) + #print(" actualErrVerbiage: %s" % actualErrVerbiage) + assert excinfo.value.__class__.__name__ == 'RallyResponseError' + ex_value_mo = re.search(expectedErrMsg, actualErrVerbiage) + assert ex_value_mo is not None time.sleep(1) - with py.test.raises(RallyRESTAPIError) as excinfo: + with py.test.raises(RallyResponseError) as excinfo: rally = Rally(server=non_rally_server, user=TRIAL_USER, password=TRIAL_PSWD, timeout=5) actualErrVerbiage = excinfo.value.args[0] # becuz Python2.6 deprecates message :-( - assert excinfo.value.__class__.__name__ == 'RallyRESTAPIError' - assert actualErrVerbiage == expectedErrMsg + assert excinfo.value.__class__.__name__ == 'RallyResponseError' time.sleep(1) def test_bad_server_spec(): diff --git a/test/test_context.py b/test/test_context.py index 4f7d434..8710643 100644 --- a/test/test_context.py +++ b/test/test_context.py @@ -2,6 +2,7 @@ import sys, os import types +import py from pyral import Rally, RallyUrlBuilder @@ -18,14 +19,14 @@ def makeResourceUrl(rally, entity, **kwargs): resource.qualify(True, None, None, 10, 1) context, augments = rally.contextHelper.identifyContext(**kwargs) ## -## print " context: %s" % repr(context) -## print "augments: %s" % repr(augments) +## print(" context: %s" % repr(context)) +## print("augments: %s" % repr(augments)) ## workspace_ref = rally.contextHelper.currentWorkspaceRef() project_ref = rally.contextHelper.currentProjectRef() ## -## print "workspace_ref: %s" % workspace_ref -## print " project_ref: %s" % project_ref +## print("workspace_ref: %s" % workspace_ref) +## print(" project_ref: %s" % project_ref) ## if workspace_ref: if 'workspace' not in kwargs or ('workspace' in kwargs and kwargs['workspace'] is not None): @@ -62,7 +63,7 @@ def test_default_context(): assert context1.project == DEFAULT_PROJECT assert project.Name == DEFAULT_PROJECT url = makeResourceUrl(rally, 'Defect') - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url expected_project_clause = 'project=project/%s' % str(project.oid) @@ -76,7 +77,7 @@ def test_explictly_set_workspace_as_default_context(): project = rally.getProject() assert project.Name == DEFAULT_PROJECT url = makeResourceUrl(rally, 'Defect') - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url expected_project_clause = 'project=project/%s' % str(project.oid) @@ -87,12 +88,19 @@ def test_initial_workspace_not_default(): rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, workspace=ALTERNATE_WORKSPACE, warn=False) + # Because no project=name arg was supplied, the project will be the User's default project + # which will not necessarily be valid for the workspace argument that was supplied workspace = rally.getWorkspace() assert workspace.Name == ALTERNATE_WORKSPACE project = rally.getProject() + assert project.Name == DEFAULT_PROJECT + + rally.setProject(ALTERNATE_PROJECT) + project = rally.getProject() assert project.Name == ALTERNATE_PROJECT + url = makeResourceUrl(rally, 'Defect') - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url expected_project_clause = 'project=project/%s' % str(project.oid) @@ -108,7 +116,7 @@ def test_explicitly_set_workspace_and_project_as_default_context(): project = rally.getProject() assert project.Name == DEFAULT_PROJECT url = makeResourceUrl(rally, 'Defect') - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url expected_project_clause = 'project=project/%s' % str(project.oid) @@ -124,7 +132,7 @@ def test_set_default_workspace_non_default_project_context(): project = rally.getProject() assert project.Name == NON_DEFAULT_PROJECT url = makeResourceUrl(rally, 'Defect') - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url expected_project_clause = 'project=project/%s' % str(project.oid) @@ -140,7 +148,7 @@ def test_set_non_default_workspace_and_project_context(): project = rally.getProject() assert project.Name == ALTERNATE_PROJECT url = makeResourceUrl(rally, 'Defect') - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url expected_project_clause = 'project=project/%s' % str(project.oid) @@ -159,7 +167,7 @@ def test_default_wksprj_with_set_workspace_with_default_context(): assert workspace.Name == DEFAULT_WORKSPACE url = makeResourceUrl(rally, 'Defect') - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url expected_project_clause = 'project=project/%s' % str(project.oid) @@ -182,6 +190,8 @@ def test_default_wksprj_with_set_non_default_workspace_context(): rally.setWorkspace(ALTERNATE_WORKSPACE) workspace = rally.getWorkspace() assert workspace.Name == ALTERNATE_WORKSPACE + + rally.setProject(ALTERNATE_PROJECT) project = rally.getProject() assert project.Name == ALTERNATE_PROJECT @@ -202,11 +212,13 @@ def test_default_workspace_with_set_non_default_workspace_context(): rally.setWorkspace(ALTERNATE_WORKSPACE) workspace = rally.getWorkspace() assert workspace.Name == ALTERNATE_WORKSPACE + + rally.setProject(ALTERNATE_PROJECT) project = rally.getProject() assert project.Name == ALTERNATE_PROJECT url = makeResourceUrl(rally, 'Defect') - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) expected_project_clause = 'project=project/%s' % str(project.oid) assert expected_workspace_clause in url @@ -229,7 +241,7 @@ def test_default_workspace_with_set_non_default_workspace_and_project_context(): assert project.Name == ALTERNATE_PROJECT url = makeResourceUrl(rally, 'Defect') - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url expected_project_clause = 'project=project/%s' % str(project.oid) @@ -244,7 +256,7 @@ def test_default_workspace_project_specify_project_equal_None_context(): assert project.Name == DEFAULT_PROJECT url = makeResourceUrl(rally, 'Defect', project=None) - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url assert '&project=' not in url @@ -260,7 +272,7 @@ def test_non_default_workspace_project_specify_project_equal_None_context(): assert project.Name == ALTERNATE_PROJECT url = makeResourceUrl(rally, 'Defect', project=None) - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url assert '&project=' not in url @@ -277,7 +289,7 @@ def test_default_wksprj_with_set_non_default_workspace_specify_project_equal_Non assert workspace.Name == ALTERNATE_WORKSPACE url = makeResourceUrl(rally, 'Defect', project=None) - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url assert '&project=' not in url @@ -298,7 +310,7 @@ def test_default_wksprj_with_set_non_default_workspace_and_project_specify_proje assert project.Name == ALTERNATE_PROJECT url = makeResourceUrl(rally, 'Defect', project=None) - #print url + #print(url) expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) assert expected_workspace_clause in url assert '&project=' not in url @@ -312,7 +324,7 @@ def test_default_wksprj_specify_workspace_and_project_equal_None_context(): assert project.Name == DEFAULT_PROJECT url = makeResourceUrl(rally, 'Defect', workspace=None, project=None) - #print url + #print(url) assert '&workspace=' not in url assert '&project=' not in url @@ -327,7 +339,7 @@ def test_non_default_wksprj_specify_workspace_and_project_equal_None_context(): assert project.Name == ALTERNATE_PROJECT url = makeResourceUrl(rally, 'Defect', workspace=None, project=None) - #print url + #print(url) assert '&workspace=' not in url assert '&project=' not in url @@ -343,12 +355,19 @@ def test_default_wksprj_set_non_default_wksprj_specify_workspace_and_project_equ workspace = rally.getWorkspace() assert workspace.Name == ALTERNATE_WORKSPACE + #problem_text = 'Specified project not valid for your current workspace or credentials' + #with py.test.raises(Exception) as excinfo: + # rally.setProject(ALTERNATE_PROJECT) + #actualErrVerbiage = excinfo.value.args[0] # becuz Python2.6 deprecates message :-( + #assert excinfo.value.__class__.__name__ == 'Exception' + #assert actualErrVerbiage == problem_text + rally.setProject(ALTERNATE_PROJECT) project = rally.getProject() assert project.Name == ALTERNATE_PROJECT - + url = makeResourceUrl(rally, 'Defect', workspace=None, project=None) - #print url + #print(url) assert '&workspace=' not in url assert '&project=' not in url diff --git a/test/test_convenience.py b/test/test_convenience.py index 40e3fd6..6b9456d 100644 --- a/test/test_convenience.py +++ b/test/test_convenience.py @@ -21,12 +21,12 @@ def test_getSchemaInfo(): """ rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) schema_info = rally.getSchemaInfo(rally.getWorkspace()) - assert type(schema_info) == types.ListType + assert type(schema_info) == list assert len(schema_info) > 50 subs_schema = [item for item in schema_info if item['Name'] == 'Subscription'] assert subs_schema != None assert len(subs_schema) == 1 - assert type(subs_schema) == types.ListType + assert type(subs_schema) == list assert u'Attributes' in subs_schema[0] assert len(subs_schema[0][u'Attributes']) > 15 @@ -89,7 +89,7 @@ def test_getAllUsers_query(): rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) everybody = rally.getAllUsers() assert len(everybody) > 0 - assert len([user for user in everybody if user.DisplayName == 'Integrations Test']) == 1 + assert len([user for user in everybody if user.DisplayName == 'da Kipster']) == 1 def test_getAllowedValues_query(): """ @@ -99,7 +99,7 @@ def test_getAllowedValues_query(): rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) avs = rally.getAllowedValues('Defect', 'State') assert len(avs) > 0 - assert len(avs) == 6 + assert len(avs) >= 4 assert u'Open' in avs assert u'Closed' in avs @@ -120,11 +120,11 @@ def test_typedef(): def test_getStates(): """ Using a known valid Rally server and known valid access credentials, - get all the State entity instances for Thme via the + get all the State entity instances for Initiative via the Rally.getStates convenience method. """ rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) - target_entity = 'Theme' + target_entity = 'Initiative' states = rally.getStates(target_entity) assert len(states) == 4 discovering = [state.Name for state in states if state.Name == "Discovering"] @@ -144,8 +144,13 @@ def test_getCollection(): response = rally.getCollection(proj_collection_url) assert response.__class__.__name__ == 'RallyRESTResponse' projects = [proj for proj in response] - assert len(projects) > 30 + assert len(projects) >= 2 assert projects.pop(0).__class__.__name__ == 'Project' + for project in projects: + print(project.details()) + print('') + print('--------------------------------------------------------') + print('') #test_getSchemaInfo() diff --git a/test/test_field_access.py b/test/test_field_access.py index f1c6c03..ceaf0cc 100644 --- a/test/test_field_access.py +++ b/test/test_field_access.py @@ -41,7 +41,7 @@ def test_defect_fields(): assert response.status_code == 200 defect = response.next() assert defect.NumberofCases == 4 - assert defect.AffectedCustomers == 'def, jkl, qrs, uvw' + assert 'def, jkl, qrs, uvw' in defect.AffectedCustomers #def test_task_fields(): # """ diff --git a/test/test_inflation.py b/test/test_inflation.py index e3dccc5..2ef1131 100644 --- a/test/test_inflation.py +++ b/test/test_inflation.py @@ -10,6 +10,7 @@ ################################################################################################## from rally_targets import TRIAL, TRIAL_USER, TRIAL_PSWD, DEFAULT_WORKSPACE, DEFAULT_PROJECT +from rally_targets import ALTERNATE_WORKSPACE ################################################################################################## @@ -67,12 +68,12 @@ def test_default_workspace_non_valid_project(): An exception should be raised. """ project = 'Halfling Leaf Pipe' - expectedErrMsg = u"Unable to use your project specification of '%s', that value is not associated with current workspace setting of: '%s'" % (project, DEFAULT_WORKSPACE) + expectedErrMsg = u"No valid Project with the name '%s' found in the Workspace '%s'" % (project, DEFAULT_WORKSPACE) with py.test.raises(Exception) as excinfo: rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, project=project) - actualErrVerbiage = excinfo.value.args[0] # becuz Python2.6 deprecates message :-( - assert excinfo.value.__class__.__name__ == 'Exception' + actualErrVerbiage = excinfo.value.args[0] + assert excinfo.value.__class__.__name__ == 'RallyRESTAPIError' assert actualErrVerbiage == expectedErrMsg def test_named_default_workspace_use_default_project(): @@ -114,7 +115,7 @@ def test_named_default_workspace_named_valid_project(): """ project = 'My Project' rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, - workspace=DEFAULT_PROJECT, project=project) + workspace=DEFAULT_WORKSPACE, project=project) response = rally.get('Project') assert response != None assert response.status_code == 200 @@ -128,12 +129,12 @@ def test_named_default_workspace_named_invalid_project(): An exception should be raised. """ project = 'Sailor Sami' - expectedErrMsg = u"Unable to use your project specification of '%s', that value is not associated with current workspace setting of: '%s'" % (project, DEFAULT_WORKSPACE) + expectedErrMsg = "No valid Project with the name '%s' found in the Workspace '%s'" % (project, DEFAULT_WORKSPACE) with py.test.raises(Exception) as excinfo: rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, workspace=DEFAULT_WORKSPACE, project=project) - actualErrVerbiage = excinfo.value.args[0] # becuz Python2.6 deprecates message :-( - assert excinfo.value.__class__.__name__ == 'Exception' + actualErrVerbiage = excinfo.value.args[0] + assert excinfo.value.__class__.__name__ == 'RallyRESTAPIError' assert actualErrVerbiage == expectedErrMsg def test_named_non_default_workspace_use_default_project(): @@ -150,17 +151,18 @@ def test_named_non_default_workspace_use_default_project(): in the named non-default workspace """ - workspace = 'SCM Workspace' - rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, workspace=workspace, warn=False) + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, + workspace=ALTERNATE_WORKSPACE, warn=False) ai_proj = rally.getProject() assert str(ai_proj.Name) == 'Sample Project' assert rally._wpCacheStatus() == 'narrow' - workspace = 'JIRA Testing' - rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, workspace=workspace, warn=False) - ai_proj = rally.getProject() - assert str(ai_proj.Name) == 'GData Testing' # is valid only in 'JIRA Testing' - assert rally._wpCacheStatus() == 'narrow' + #alt_project = "Modus Operandi" + #rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, + # workspace=ALTERNATE_WORKSPACE, project=alt_project, warn=False) + #ai_proj = rally.getProject() + #assert str(ai_proj.Name) == alt_project # is valid only in ALTERNATE_WORKSPACE + #assert rally._wpCacheStatus() == 'narrow' def test_named_non_default_workspace_named_valid_project(): """ @@ -169,30 +171,34 @@ def test_named_non_default_workspace_named_valid_project(): Return status should be OK, the Rally instance's RallyContextHelper _inflated value should be 'minimal' """ - workspace = 'JIRA Manual Testing' - project = 'Another Sample Project' - rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, - workspace=workspace, project=project) + workspace = "Kip's Playground" + alt_project = 'Modus Operandi' + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, workspace=workspace, warn=False) response = rally.get('Project') assert response != None assert response.status_code == 200 assert rally._wpCacheStatus() == 'narrow' + rally.setProject(alt_project) + proj = rally.getProject() + assert proj.Name == alt_project + + def test_named_non_default_workspace_named_invalid_project(): """ Using valid Rally access credentials, connect specifying a valid non-default workspace and an invalid project. An exception should be raised. """ - workspace = 'JIRA Manual Testing' + workspace = ALTERNATE_WORKSPACE project = 'Barney Rubble' - expectedErrMsg = u"Unable to use your project specification of '%s', that value is not associated with current workspace setting of: '%s'" % (project, workspace) + expectedErrMsg = u"No valid Project with the name '%s' found in the Workspace '%s'" % (project, workspace) with py.test.raises(Exception) as excinfo: rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, workspace=workspace, project=project, timeout=10) response = rally.get('Project', fetch=False, limit=5) - actualErrVerbiage = excinfo.value.args[0] # becuz Python2.6 deprecates message :-( - assert excinfo.value.__class__.__name__ == 'Exception' + actualErrVerbiage = excinfo.value.args[0] + assert excinfo.value.__class__.__name__ == 'RallyRESTAPIError' assert actualErrVerbiage == expectedErrMsg ######################################################################################## diff --git a/test/test_query.py b/test/test_query.py index 9403599..4ad6728 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -5,6 +5,11 @@ import urllib import py +try: + from urllib import unquote +except: + from urllib.parse import unquote + from pyral import Rally import pyral @@ -14,7 +19,7 @@ ################################################################################################## from rally_targets import TRIAL, TRIAL_USER, TRIAL_PSWD -from rally_targets import DEFAULT_WORKSPACE, NON_DEFAULT_PROJECT +from rally_targets import DEFAULT_WORKSPACE, DEFAULT_PROJECT, NON_DEFAULT_PROJECT ################################################################################################## @@ -55,8 +60,7 @@ def test_all_fields_query(): response = rally.get('Project', fetch=True, limit=10) assert response.status_code == 200 assert len(response.errors) == 0 - #assert len(response._page) == 12 - assert response.resultCount > 12 + assert response.resultCount > 1 for project in response: assert project.oid > 0 assert len(project.Name) > 0 @@ -128,7 +132,7 @@ def test_multiple_entities_query(): multiple_entities = "Project,Workspace" with py.test.raises(InvalidRallyTypeNameError) as excinfo: response = rally.get(multiple_entities, fetch=False, limit=10) - actualErrVerbiage = excinfo.value.args[0] # becuz Python2.6 deprecates message :-( + actualErrVerbiage = excinfo.value.args[0] assert excinfo.value.__class__.__name__ == 'InvalidRallyTypeNameError' assert actualErrVerbiage == multiple_entities @@ -144,6 +148,7 @@ def test_multiple_page_response_query(): count = 0 for ix, bugger in enumerate(response): count += 1 + assert response.resultCount > 5 assert count <= response.resultCount assert count == 15 @@ -168,8 +173,8 @@ def test_defects_revision_history(): d1_revs = defect1.RevisionHistory.Revisions d2_revs = defect2.RevisionHistory.Revisions - assert type(d1_revs) == types.ListType - assert type(d2_revs) == types.ListType + assert type(d1_revs) == list + assert type(d2_revs) == list d1_rev1 = d1_revs.pop() # now the revs are in stack order, newest first, original the last d2_rev1 = d2_revs.pop() # ditto @@ -279,8 +284,8 @@ def test_three_condition_query_in_list(): def test_five_condition_query_in_list(): rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) qualifiers = ["State = Submitted", - "FormattedID < DE6000", - "FormattedID != DE5986", + "FormattedID < DE22", + "FormattedID != DE17", 'Priority = "High Attention"', "Severity != Cosmetic" ] @@ -323,30 +328,31 @@ def test_limit_query(): """ rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) qualifier = "State = Submitted" - response = rally.get('Defect', fetch=True, query=qualifier, pagesize=200, limit=80) + response = rally.get('Defect', fetch=True, query=qualifier, pagesize=100, limit=30) items = [item for item in response] - assert len(items) == 80 + assert len(items) == 30 def test_start_value_query(): """ - Use a pagesize of 200 and a start index value of 300 in the params in the URL + Use a pagesize of 200 and a start index value of 10 in the params in the URL """ rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) qualifier = "State = Submitted" - response = rally.get('Defect', fetch=True, query=qualifier, pagesize=200, start=2000) + response = rally.get('Defect', fetch=True, query=qualifier, pagesize=200, start=10) items = [item for item in response] - assert len(items) > 200 - assert len(items) < 600 + assert len(items) > 20 + assert len(items) < 1000 def test_start_and_limit_query(): """ - Use a pagesize of 50 and a start index value of 20 and a limit of 60 in the params in the URL + Use a pagesize of 50 and a start index value of 10 and a limit of 40 in the params in the URL """ rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) qualifier = "State = Submitted" - response = rally.get('Defect', fetch=True, query=qualifier, pagesize=50, start=20,limit=60) + response = rally.get('Defect', fetch=True, query=qualifier, pagesize=50, start=10,limit=40) items = [item for item in response] - assert len(items) == 60 + assert len(items) > 10 + assert len(items) <= 40 def test_query_target_value_with_ampersand(): """ @@ -354,19 +360,19 @@ def test_query_target_value_with_ampersand(): """ criteria = ['Project.Name = R&D'] result = RallyQueryFormatter.parenGroups(criteria) - assert urllib.unquote(result) == 'Project.Name = R&D'.replace('&', '%26') + assert unquote(result) == 'Project.Name = R&D'.replace('&', '%26') criteria = ['Project.Name = "R&D"'] result = RallyQueryFormatter.parenGroups(criteria) - assert urllib.unquote(result) == 'Project.Name = "R&D"'.replace('&', '%26') + assert unquote(result) == 'Project.Name = "R&D"'.replace('&', '%26') criteria = ['Project.Name contains "R&D"'] result = RallyQueryFormatter.parenGroups(criteria) - assert urllib.unquote(result) == 'Project.Name contains "R&D"'.replace('&', '%26') + assert unquote(result) == 'Project.Name contains "R&D"'.replace('&', '%26') criteria = 'Railhead.Company.Name != "Atchison Topeka & Santa Fe & Cunard Lines"' result = RallyQueryFormatter.parenGroups(criteria) - assert urllib.unquote(result) == criteria.replace('&', '%26') + assert unquote(result) == criteria.replace('&', '%26') def test_query_target_value_with_and(): @@ -383,12 +389,12 @@ def test_query_target_value_with_and(): def test_query_with_special_chars_in_criteria(): """ - DE3228 in DEFAULT_WORKSPACE / NON_DEFAULT_PROJECT has Name = Special chars:/!@#$%^&*()-=+[]{};:./<>?/ + DE3228 in DEFAULT_WORKSPACE / DEFAULT_PROJECT has Name = Special chars:/!@#$%^&*()-=+[]{};:./<>?/ query for it by looking for it by the name value """ rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) rally.setWorkspace(DEFAULT_WORKSPACE) - rally.setProject(NON_DEFAULT_PROJECT) + rally.setProject(DEFAULT_PROJECT) rally.enableLogging('spec_char_query') criteria = 'Name = "distinctive criteria of -32% degradation in rust protection"' response = rally.get('Defect', fetch=True, query=criteria, limit=10) @@ -405,7 +411,7 @@ def test_query_with_special_chars_in_criteria(): assert response.warnings == [] assert response.resultCount == 1 - special_chars = "/!@#$%^*_-+=?{}[]:;,<>" + special_chars = "/!@#$%^*_-+=?{}[]:;,.<>" # characters that break the RallyQueryFormatter and/or WSAPI: ( ) ~ & | backslash for character in special_chars: criteria = 'Name contains "%s"' % character @@ -416,7 +422,7 @@ def test_query_with_special_chars_in_criteria(): assert response.warnings == [] assert response.resultCount >= 1 - criteria = 'Name = "Special chars:/!@#$%^*-=+[]{};:.<>? in the name field"' + criteria = 'Name = "Special chars:/!@#$%^*-=+[]{};:,.<>? in the name field"' response = rally.get('Defect', fetch=True, query=criteria, limit=10) assert response.__class__.__name__ == 'RallyRESTResponse' assert response.status_code == 200 @@ -426,13 +432,10 @@ def test_query_with_special_chars_in_criteria(): def test_query_with_matched_parens_in_condition_value(): """ - 'REST Toolkit Testing' / 'Sample Project' has a Release in it whose name contains a matched paren pair + The default workspace and project has a Release in it whose name contains a matched paren pair make sure a query containing a condition looking for the Release by this name succeeds. """ - target_workspace = 'REST Toolkit Testing' - target_project = 'Sample Project' - - rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, workspace=target_workspace, project=target_project) + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) rally.enableLogging('query_condition_value_has_matched_internal_parens') criteria = 'Name = "8.5 (Blah and Stuff)"' diff --git a/test/test_ranking.py b/test/test_ranking.py new file mode 100644 index 0000000..0d2dbc0 --- /dev/null +++ b/test/test_ranking.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +import sys, os +import pprint + +from pyral import Rally + +################################################################################################## + +from rally_targets import TRIAL, TRIAL_USER, TRIAL_PSWD + +################################################################################################## + +def test_rank_story_above(): + """ + Using a known valid Rally server and known valid access credentials, + obtain a Rally instance and issue a get for Story items ordered by Rank ASC. + """ + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, server_ping=False, isolated_workspace=True) + response = rally.get('Story', fetch="FormattedID,Name,Description,State,DragAndDropRank", + order="Rank ASC", limit=100) + stories = [story for story in response] + assert len(stories) > 6 + + story1 = stories[0] + story2 = stories[4] + assert story1.DragAndDropRank < story2.DragAndDropRank + result = rally.rankAbove(story1, story2) + assert result.status_code == 200 + + response = rally.get('Story', fetch="FormattedID,Name,Description,State,DragAndDropRank", + order="Rank ASC", limit=100) + stories = [story for story in response] + assert stories[0].FormattedID == story2.FormattedID + assert stories[0].DragAndDropRank < story2.DragAndDropRank + +def test_rank_story_below(): + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, server_ping=False, isolated_workspace=True) + response = rally.get('Story', fetch="FormattedID,Name,Description,State,DragAndDropRank", + order="Rank ASC", limit=100) + stories = [story for story in response] + assert len(stories) > 6 + + story1 = stories[2] + story2 = stories[4] + assert story1.DragAndDropRank < story2.DragAndDropRank + + result = rally.rankBelow(story2, story1) + assert result.status_code == 200 + + response = rally.get('Story', fetch="FormattedID,Name,Description,State,DragAndDropRank", + order="Rank ASC", limit=100) + stories = [story for story in response] + assert stories[3].FormattedID == story2.FormattedID + assert stories[4].DragAndDropRank > story2.DragAndDropRank + + + +def test_rank_story_to_top(): + """ + """ + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, server_ping=False, isolated_workspace=True) + response = rally.get('Story', fetch="FormattedID,Name,Description,State,DragAndDropRank", + order="Rank ASC", limit=100) + stories = [story for story in response] + assert len(stories) > 6 + + first_story = stories[0] + target_story = stories[4] + assert first_story.DragAndDropRank < target_story.DragAndDropRank + result = rally.rankToTop(target_story) + assert result.status_code == 200 + + response = rally.get('Story', fetch="FormattedID,Name,Description,State,DragAndDropRank", + order="Rank ASC", limit=100) + stories = [story for story in response] + top_story = stories[0] + bottom_story = stories[-1] + assert top_story.FormattedID == target_story.FormattedID + assert top_story.DragAndDropRank < stories[1].DragAndDropRank + +def test_rank_story_to_bottom(): + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, server_ping=False, isolated_workspace=True) + response = rally.get('Story', fetch="FormattedID,Name,Description,State,DragAndDropRank", + order="Rank ASC", limit=100) + stories = [story for story in response] + assert len(stories) > 6 + + first_story = stories[0] + target_story = stories[1] + assert target_story.DragAndDropRank > first_story.DragAndDropRank + + result = rally.rankToBottom(target_story) + assert result.status_code == 200 + response = rally.get('Story', fetch="FormattedID,Name,Description,State,DragAndDropRank", + order="Rank ASC", limit=100) + stories = [story for story in response] + top_story = stories[0] + bottom_story = stories[-1] + assert bottom_story.FormattedID == target_story.FormattedID + diff --git a/test/test_recyclebin.py b/test/test_recyclebin.py new file mode 100644 index 0000000..e4977af --- /dev/null +++ b/test/test_recyclebin.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +import sys, os +import types +import urllib +import py + +from pyral import Rally +import pyral + +InvalidRallyTypeNameError = pyral.entity.InvalidRallyTypeNameError +from pyral.query_builder import RallyUrlBuilder, RallyQueryFormatter + +################################################################################################## + +from rally_targets import TRIAL, TRIAL_USER, TRIAL_PSWD +from rally_targets import DEFAULT_WORKSPACE, NON_DEFAULT_PROJECT + +################################################################################################## + +def test_basic_query(): + """ + Using a known valid Rally server and known valid access credentials, + issue a simple filtering query targeting RecycleBinEntry items + whose Name value does not contain a specific value. + """ + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) + response = rally.get('RecycleBinEntry', fetch="ObjectID,ID,Name", + query='Name = "Gone but not forgotten with the wind"', + limit=100) + assert response.status_code == 200 + assert response.errors == [] + assert response.warnings == [] + assert response.resultCount > 0 + #print(response.resultCount) + #for entry in response: + # print("{:14d} {:8s} {:s}".format(entry.ObjectID, entry.ID, entry.Name)) + +#test_basic_query() + diff --git a/test/test_search.py b/test/test_search.py new file mode 100644 index 0000000..3d6d39b --- /dev/null +++ b/test/test_search.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +import sys, os +import types +import urllib +import py + +from pyral import Rally +import pyral + +InvalidRallyTypeNameError = pyral.entity.InvalidRallyTypeNameError +from pyral.query_builder import RallyUrlBuilder, RallyQueryFormatter + +################################################################################################## + +from rally_targets import TRIAL, TRIAL_USER, TRIAL_PSWD +from rally_targets import DEFAULT_WORKSPACE, NON_DEFAULT_PROJECT + +################################################################################################## + +def test_basic_search(): + """ + Using a known valid Rally server and known valid access credentials, + issue a simple search query (basic qualifying criteria). + """ + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) + # + #expectedErrMsg = u'The new search functionality is not turned on for your subscription' + # + #with py.test.raises(RallyRESTAPIError) as excinfo: + # response = rally.search('wombat', limit=10) + # actualErrVerbiage = excinfo.value.args[0] + # assert excinfo.value.__class__.__name__ == 'RallyRESTAPIError' + # assert expectedErrMsg in actualErrVerbiage + response = rally.search('bogus', limit=10) + assert response.status_code == 200 + assert response.errors == [] + assert response.warnings == [] + assert response.resultCount > 0 + print(response.resultCount) + for entry in response: + print(entry.ObjectID, entry.FormattedID, entry.Name) + + +#def test_simple_named_fields_query(): +# """ +# Using a known valid Rally server and known valid access credentials, +# issue a simple query (no qualifying criteria) for a known valid +# Rally entity. The fetch specifies a small number of known valid +# attributes on the Rally entity. +# """ +# rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD) +# response = rally.search('yellow display", limit=10) +# assert response.status_code == 200 +# assert len(response.errors) == 0 +# assert len(response._page) > 0 + +#test_basic_search() +#test_simple_named_fields_query() diff --git a/test/test_workspaces.py b/test/test_workspaces.py new file mode 100644 index 0000000..730f9db --- /dev/null +++ b/test/test_workspaces.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python + +import sys, os +import types +import py + +from pyral import Rally, RallyUrlBuilder + +################################################################################################## + +from rally_targets import TRIAL, TRIAL_USER, TRIAL_PSWD +from rally_targets import DEFAULT_WORKSPACE, DEFAULT_PROJECT, NON_DEFAULT_PROJECT +from rally_targets import ALTERNATE_WORKSPACE, ALTERNATE_PROJECT + +################################################################################################## + +def makeResourceUrl(rally, entity, **kwargs): + resource = RallyUrlBuilder(entity) + resource.qualify(True, None, None, 10, 1) + context, augments = rally.contextHelper.identifyContext(**kwargs) +## +## print(" context: %s" % repr(context)) +## print("augments: %s" % repr(augments)) +## + workspace_ref = rally.contextHelper.currentWorkspaceRef() + project_ref = rally.contextHelper.currentProjectRef() +## +## print("workspace_ref: %s" % workspace_ref) +## print(" project_ref: %s" % project_ref) +## + if workspace_ref: + if 'workspace' not in kwargs or ('workspace' in kwargs and kwargs['workspace'] is not None): + resource.augmentWorkspace(augments, workspace_ref) + if project_ref: + if 'project' not in kwargs or ('project' in kwargs and kwargs['project'] is not None): + resource.augmentProject(augments, project_ref) + resource.augmentScoping(augments) + + url = "%s/%s" % (rally.service_url, resource.build()) + return url + +################################################################################################## + +# 1 +def test_default_context(): + """ + Using a known valid Rally server and known valid access credentials, + obtain a Rally instance and confirm that the default workspace + and project are set to DEFAULT_WORKSPACE and DEFAULT_PROJECT and + that the current workspace and project are indeed the DEFAULT_WORKSPACE + and DEFAULT_PROJECT values. + Furthermore the construction of a GET related URL will contain + the correct workspace and project specifications in the QUERY_STRING. + """ + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, server_ping=False) + context1 = rally.contextHelper.currentContext() + workspace = rally.getWorkspace() + project = rally.getProject() + context2 = rally.contextHelper.currentContext() + assert context1 == context2 + assert context1.workspace == DEFAULT_WORKSPACE + assert workspace.Name == DEFAULT_WORKSPACE + assert context1.project == DEFAULT_PROJECT + assert project.Name == DEFAULT_PROJECT + url = makeResourceUrl(rally, 'Defect') + #print(url) + expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) + assert expected_workspace_clause in url + expected_project_clause = 'project=project/%s' % str(project.oid) + assert expected_project_clause in url + +# 2 +def test_default_isolated_workspace(): + """ + Using a known valid Rally server and known valid access credentials, + obtain a Rally instance and confirm that the default workspace + and project are set to DEFAULT_WORKSPACE and DEFAULT_PROJECT and + that the current workspace and project are indeed the DEFAULT_WORKSPACE + and DEFAULT_PROJECT values. + Furthermore the construction of a GET related URL will contain + the correct workspace and project specifications in the QUERY_STRING. + And any attempt to change the workspace via rally.setWorkspace(some_name) + will result in a Exception being raised + """ + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, server_ping=False, isolated_workspace=True) + context1 = rally.contextHelper.currentContext() + workspace = rally.getWorkspace() + project = rally.getProject() + context2 = rally.contextHelper.currentContext() + assert context1 == context2 + assert context1.workspace == DEFAULT_WORKSPACE + assert workspace.Name == DEFAULT_WORKSPACE + assert context1.project == DEFAULT_PROJECT + assert project.Name == DEFAULT_PROJECT + url = makeResourceUrl(rally, 'Defect') + #print(url) + expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) + assert expected_workspace_clause in url + + problem_text = 'No reset of of the Workspace is permitted when the isolated_workspace option is specified' + with py.test.raises(Exception) as excinfo: + rally.setWorkspace(ALTERNATE_WORKSPACE) + actualErrVerbiage = excinfo.value.args[0] + assert excinfo.value.__class__.__name__ == 'RallyRESTAPIError' + assert actualErrVerbiage == problem_text + +# 3 +def test_explictly_set_workspace_as_default_context(): + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, workspace=DEFAULT_WORKSPACE) + workspace = rally.getWorkspace() + assert workspace.Name == DEFAULT_WORKSPACE + project = rally.getProject() + assert project.Name == DEFAULT_PROJECT + url = makeResourceUrl(rally, 'Defect') + #print(url) + expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) + assert expected_workspace_clause in url + expected_project_clause = 'project=project/%s' % str(project.oid) + assert expected_project_clause in url + + rally.setWorkspace(ALTERNATE_WORKSPACE) + assert rally.getWorkspace().Name == ALTERNATE_WORKSPACE + +# 4 +def test_explictly_set_workspace_as_isolated_workspace(): + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, workspace=DEFAULT_WORKSPACE, isolated_workspace=True) + workspace = rally.getWorkspace() + assert workspace.Name == DEFAULT_WORKSPACE + project = rally.getProject() + assert project.Name == DEFAULT_PROJECT + url = makeResourceUrl(rally, 'Defect') + #print(url) + expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) + assert expected_workspace_clause in url + expected_project_clause = 'project=project/%s' % str(project.oid) + + problem_text = 'No reset of of the Workspace is permitted when the isolated_workspace option is specified' + with py.test.raises(Exception) as excinfo: + rally.setWorkspace(ALTERNATE_WORKSPACE) + actualErrVerbiage = excinfo.value.args[0] + assert excinfo.value.__class__.__name__ == 'RallyRESTAPIError' + assert actualErrVerbiage == problem_text + +# 5 +def test_initial_workspace_not_default(): + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, + workspace=ALTERNATE_WORKSPACE, + warn=False) + # Because no project=name arg was supplied, the project will be the User's default project + # which will not necessarily be valid for the workspace argument that was supplied + workspace = rally.getWorkspace() + assert workspace.Name == ALTERNATE_WORKSPACE + project = rally.getProject() + assert project.Name == DEFAULT_PROJECT + + rally.setProject(ALTERNATE_PROJECT) + project = rally.getProject() + assert project.Name == ALTERNATE_PROJECT + + url = makeResourceUrl(rally, 'Defect') + #print(url) + expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) + assert expected_workspace_clause in url + expected_project_clause = 'project=project/%s' % str(project.oid) + assert expected_project_clause in url + + rally.setWorkspace(DEFAULT_WORKSPACE) + workspace = rally.getWorkspace() + assert workspace.Name == DEFAULT_WORKSPACE + + rally.setProject(DEFAULT_PROJECT) + project = rally.getProject() + assert project.Name == DEFAULT_PROJECT + + url = makeResourceUrl(rally, 'Defect') + #print(url) + expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) + assert expected_workspace_clause in url + expected_project_clause = 'project=project/%s' % str(project.oid) + assert expected_project_clause in url + +# 6 +def test_initial_non_default_workspace_as_isolated(): + rally = Rally(server=TRIAL, user=TRIAL_USER, password=TRIAL_PSWD, + workspace=ALTERNATE_WORKSPACE, + warn=False, isolated_workspace=True) + # Because no project=name arg was supplied, the project will be the User's default project + # which will not necessarily be valid for the workspace argument that was supplied + workspace = rally.getWorkspace() + assert workspace.Name == ALTERNATE_WORKSPACE + project = rally.getProject() + assert project.Name == DEFAULT_PROJECT + + rally.setProject(ALTERNATE_PROJECT) + project = rally.getProject() + assert project.Name == ALTERNATE_PROJECT + + url = makeResourceUrl(rally, 'Defect') + #print(url) + expected_workspace_clause = 'workspace=workspace/%s' % str(workspace.oid) + assert expected_workspace_clause in url + expected_project_clause = 'project=project/%s' % str(project.oid) + assert expected_project_clause in url + + problem_text = 'No reset of of the Workspace is permitted when the isolated_workspace option is specified' + with py.test.raises(Exception) as excinfo: + rally.setWorkspace(DEFAULT_WORKSPACE) + actualErrVerbiage = excinfo.value.args[0] + assert excinfo.value.__class__.__name__ == 'RallyRESTAPIError' + assert actualErrVerbiage == problem_text