跳转到主要内容

VersionOne API客户端

项目描述

VersionOne Python SDK

通过PyPi(pip)正式分发为: v1pysdk
此软件包的较早版本,与该版本号相关联,分发为'v1pysdk-unoffical'

VersionOne Python SDK 是一个用于 VersionOne API 的开源和社区支持的客户端。

作为一个开源和社区支持的项目,VersionOne Python SDK 并非由 VersionOne 正式支持。

话虽如此,仍有多种方式来解决您的问题。

一般来说,StackOverflow 是获取 VersionOne Python SDK 支持的最佳选择。

VersionOne Python SDK 的源代码是免费和开源的,我们鼓励您通过 提交拉取请求 来改进它!

概述

实例化连接

为了交互,您必须首先创建一个 V1Meta 对象的实例。这需要您指定如何连接到服务器。

有两种选择,直接指定实例的完整 URL,或指定详细信息。

from v1pysdk import V1Meta

with V1Meta(
  instance_url = 'http://localhost/VersionOne',
  # any instance, scheme, or address values will be ignored
  username = 'admin',
  password = 'admin'
  ) as v1:

或者

from v1pysdk import V1Meta

with V1Meta(
  address = 'localhost',
  instance = 'VersionOne',
  scheme = 'http', #optional, defaults to https
  username = 'admin',
  password = 'admin'
  ) as v1:

为了进行身份验证,提供了两种方法,如上所述的用户名和密码,或者访问令牌。令牌是通过通过 Web 界面登录到 VersionOne、转到用户的个人资料、转到应用程序并创建新应用程序创建的。这将提供一个类似于 1.2cFHe7NkoO1kOV/x8WLpw1NasJg= 的访问令牌。请保密,因为它是您实例上特定用户的秘密 API 访问密钥。

要使用访问令牌,您将其用作密码,并设置标志以指示它实际上是一个访问令牌。这将不再需要存在用户名。

from v1pysdk import V1Meta

with V1Meta(
  address = 'localhost',
  instance = 'VersionOne',
  password = '1.2cFHe7NkoO1kOV/x8WLpw1NasJg=',
  use_password_as_token=True,
  ) as v1:

所有 V1 资产类型的动态反射

只需实例化一个 V1Meta。服务器上定义的所有资产类型都可作为实例上的属性使用。元数据只加载一次,因此您必须创建一个新的 V1Meta 实例以获取元数据更改。每个资产类都附带所有资产属性和操作的属性。

from v1pysdk import V1Meta

with V1Meta(
  instance_url = 'http://localhost/VersionOne',
  username = 'admin',
  password = 'admin'
  ) as v1:

  user = v1.Member(20) # internal numeric ID

  print user.CreateDate, user.Name

简单访问单个资产

资产实例在需要时创建并缓存,以确保具有相同 OID 的实例始终是同一对象。您可以通过将资产 ID 传递给资产类来检索实例。

      s = v1.Story(1005)

或通过提供 OID 令牌

      s = v1.asset_from_oid('Story:1005')

      print s is v1.Story(1005)   # True

延迟加载的值和关系

注意:对每个对象上的属性访问进行同步请求是昂贵的。我们建议使用查询语法来选择、过滤、聚合和检索相关资产中的值,以在单个 HTTP 事务中完成。

资产实例在创建时为空,或者在可用的情况下带有查询结果。对于尚未检索的属性,将访问服务器。在首次找到未找到的属性时,将检索一组基本属性。

      epic = v1.Epic(324355)

      # No data fetched yet.
      print epic  #=>  Epic(324355)

      # Access an attribute.
      print epic.Name  #=> "Team Features"

      # Now some basic data has been fetched
      print epic       #=> Epic(324355).with_data({'AssetType': 'Epic',
                           'Description': "Make features easier for new team members", 'AssetState': '64',
                           'SecurityScope_Name': 'Projects', 'Number': 'E-01958', 'Super_Number': 'E-01902',
                           'Scope_Name': 'Projects', 'Super_Name': 'New Feature Development',
                           'Scope': [Scope(314406)], 'SecurityScope': [Scope(314406)],
                           'Super': [Epic(312659)], 'Order': '-24', 'Name': 'Team Features'})
  # And further non-basic data is available, but will cause a request.
      print epic.CreateDate   #=>  '2012-05-14T23:45:14.124'

可以随意遍历关系网络,并且资产将在需要时检索。

    # Freely traverse the relationship graph
    print epic.Super.Scope.Name  #=> 'Products'

由于元数据被建模为数据,您可以找到“基本”属性列表。

    basic_attr_names = list( v1.AttributeDefinition
                               .where(IsBasic = "true")
                               .select('Name')
                               .Name
                           )

操作

可以通过在资产实例上调用适当的方法来启动资产的操作。

      for story in epic.Subs:
        story.QuickSignup() 

在成功后,资产实例数据将被无效化,因此在下一次属性访问时将重新检索。

迭代通过类型的所有资产

资产类是可迭代的,可以获得该类型的所有资产。在没有提供参数的情况下,这相当于“查询”、“选择”或“where”方法。

      # WARNING: Lots of HTTP requests this way.
      members = list(v1.Member)                               # HTTP request to get the list of members.
      print "Members: " + ', '.join(m.Name for m in members)  # HTTP request per member to fetch the Name

      # A much better way, requiring a single HTTP access via the query mechanism.
      members = v1.Member.select('Name')
      print "Members: " + ', '.join(m.Name for m in members)  # HTTP request to return list of members with Name attribute.

      # There is also a shortcut for pulling an attribute off all the results
      members = v1.Member.select('Name')
      print "Members: " + ', '.join(members.Name)

      # Alternative to best way with more explicit indication of what's being done
      members = v1.Member.select('Name')
      members.queryAll()   # forces performing HTTP query to fetch all members' basic details
      print "Members: " + ', '.join(m.Name for m in members)

查询

查询对象

资产实例上的 select()where()sort() 方法在查询对象上返回,您可以在其上调用更多的 .where().select().sort()。遍历查询对象将运行查询。

查询对象上的 .first().queryAll().reQueryAll() 方法将立即运行查询并返回适当的结果。

可以使用 find() 在字段上执行服务器端全字匹配,尽管它对服务器性能有较大影响,且只能匹配一个字段,因此应谨慎使用。

可以使用 page() 限制结果,以在服务器端执行分页。

reQueryAll() 可以像 queryAll() 一样使用,但如果查询选项有任何更改,将清除所有之前缓存的データ并重新运行 HTTP 查询,这使得在仅更改响应限制(如 page())时重复查询变得容易。

简单查询语法

使用 .where(Attr="value", ...) 引入“等于”比较,并使用 .select("Attr", ...) 添加到选择列表中。

不支持“不等于”比较(请使用高级查询语法)。

      for s in v1.Story.where(Name='Add feature X to main product"):
          print s.Name, s.CreateDate, ', '.join([owner.Name for owner in s.Owners])

      # Select only some attributes to reduce traffic

      for s in v1.Story.select('Name', 'Owners').where(Estimate='10'):
          print s.Name, [o.Name for o in s.Owners]

高级查询,采用标准 V1 查询语法。

filter() 操作符可以接受任意的 V1 过滤项。

      for s in (v1.Story
                  .filter("Estimate>'5';TotalDone.@Count<'10'")
                  .select('Name')):
          print s.Name

通过分页限制来自服务器的结果

客户端可以通过限制返回的与查询匹配的结果数量,让服务器执行分页,这可能更容易。分页需要限制返回的项目数量,以及要返回的列表中第一个项目的索引。

API 允许省略索引,这假定默认起始索引为 0。

    pageNum = 0
    pageSize = 3
    pageStart = 0
    while True:
        results = ( v1.Story
                     .select('Name')
                     .filter(str(myFilter))
                     .sort('-Name')
                     .page(size=pageSize, start=pageStart) 
                  ) # Requires a new query each time
        if not len(results):
            break;
        print("Page items = " + str(len(results)))
        pageNum += 1
        pageStart += pageSize
        print("Page " + str(pageNum) + " : " + ',   '.join(results.Name))

或者可以使用 reQueryAll() 根据更新的查询设置强制重新查询内容,以便更容易实现分页。

    pageNum = 0
    pageSize = 3
    pageStart = 0
    results = ( v1.Story
                .select('Name')
                .filter(str(myFilter))
                .sort('-Name')
               )

    while True:
        results = results.page(size=pageSize, start=pageStart).reQueryAll()
        if not len(results):
            break;
        pageNum += 1
        pageStart += pageSize
        print("Page " + str(pageNum) + " : " + ',   '.join(results.Name))

排序

可以通过指定要排序的列的顺序以及这些列是否应该升序或降序排序,在查询中包含排序。默认排序顺序是升序。

sort() 操作符与 select() 操作符类似,字段名称用引号括起来,可以作为单个排序调用的单独参数、单独的排序调用或两者的混合来列出。降序排序需要字段名称前加上负号“-”。
字段只能按排序顺序列出一次,重复将被忽略。

按名称的逆字母顺序排序,然后按估算时间排序,最后按详细估算时间排序

    results = v1.Story.select('Name').filter(str(myFilter)).sort('-Name','Estimate').sort('DetailedEstimate')
    print '\n'.join(results.Name)

匹配搜索

虽然可以进行搜索,但搜索非常占用服务器资源,应尽可能避免。服务器端搜索可以在单个字段内进行全字匹配。因此,应该使用适当的过滤/where 命令将其显著限制。

    results = v1.Story.select('Name').filter(str(myFilter)).find('Get a', field='Name')
    print ', '.join(results.Name) #=> Get a handle on filtering, Get a toolkit for ease of use

高级选择,采用标准 V1 选择语法。

select() 操作符将允许任意的 V1 “选择”项,并将它们添加到结果的“数据”映射中,其键与使用的项相同。

    select_term = "Workitems:PrimaryWorkitem[Status='Done'].Estimate.@Sum"
    total_done = ( v1.Timebox
                     .where(Name="Iteration 25")
                     .select(select_term)
                 )
    for result in total_done:
      print "Total 'Done' story points: ", result.data[select_term]

高级过滤和选择

获取所有人们正在努力完成的故事列表

      writer = csv.writer(outfile)
      results = (
        v1.Story
          .select('Name', 'CreateDate', 'Estimate', 'Owners.Name')
          .filter("Owners.OwnedWorkitems.@Count='1'")
          )
      for result in results:
          writer.writerow((result['Name'], ', '.join(result['Owners.Name'])))

简单创建语法

注意:所有“必需”属性都必须设置,否则服务器将拒绝数据。

      from v1pysdk import V1Meta
      v1 = V1Meta(username='admin', password='admin')
      new_story = v1.Story.create(
        Name = 'New Story',
        Scope = v1.Scope.where(Name='2012 Projects').first()
        )
      # creation happens immediately. No need to commit.
      print new_story.CreateDate
      new_story.QuickSignup()
      print 'Owners: ' + ', '.join(o.Name for o in story.Owners)

简单更新语法。

直到调用 V1Meta.commit() 之前都不会写入任何内容,然后所有脏资产将被写出。

      story = v1.Story.where(Name='Super Cool Feature do over').first()
      story.Name = 'Super Cool Feature Redux'
      story.Owners = v1.Member.where(Name='Joe Koberg')      
      errors = v1.commit()  # flushes all pending updates to the server
      if not errors:
          print("Successfully committed!")
      else:
          for e in errors:
              raise e 

V1Meta 对象还充当上下文管理器,在退出时将提交脏对象。

      with V1Meta() as v1:
        story = v1.Story.where(Name='New Features').first()
        story.Owners = v1.Member.where(Name='Joe Koberg')

      print "Story committed implicitly."

附件内容

可以使用附件实例的特殊“file_data”属性获取或设置附件文件体。

有关完整示例,请参阅 v1pysdk/tests/test_attachment.py 文件。

自 / 历史查询

查询可以返回过去特定时间点的数据。查询术语 .asof() 可以接受一个时间戳列表(或多个位置参数),或者以 ISO 日期格式表示的字符串。查询针对列表中的每个时间戳运行。返回一个单次可迭代的集合,将迭代所有收集到的结果。所有结果都将包含一个名为 'AsOf' 的数据项,其中包含该项目的“自”日期。
请注意,“自”日期不是项目的上一个更改日期,而是查询中传入的确切相同日期。
请注意,像“2012-01-01”这样的时间戳被认为是当天午夜开始的时间,这自然排除了当天发生的任何活动。您可能希望指定一个具有特定小时或下一天时间戳的时间戳。执行这些比较时使用的时区是V1Meta对象中指定的用户配置的时区,时间比较是基于服务器确定的时间进行的。

      with V1Meta() as v1:
        results = (v1.Story
                     .select("Owners")
                     .where(Name="Fix HTML5 Bug")
                     .asof("2012-10-10", "2012-10-11")
                  )
        for result in results:
            print result.data['AsOf'], [o.Name for o in result.Owners]

轮询(待办事项)

将提供简单的回调API来挂钩资产更改

      from v1meta import V1Meta
      from v1poll import V1Poller

      MAILBODY = """
      From: VersionOne Notification <notifier@versionone.mycorp.com>
      To: John Smith <cto@mycorp.com>

      Please take note of the high risk story '{0}' recently created in VersionOne.

      Link: {1}


      Thanks,

      Your VersionOne Software
      """.lstrip()

      def notify_CTO_of_high_risk_stories(story):
        if story.Risk > 10:
            import smtplib, time
            server = smtplib.SMTP('smtp.mycorp.com')
            server.sendmail(MAILBODY.format(story.Name, story.url))
            server.quit()
            story.CustomNotificationLog = (story.CustomNotificationLog +
                "\n Notified CTO on {0}".format(time.asctime()))

      with V1Meta() as v1:
        with V1Poller(v1) as poller:
          poller.run_on_new('Story', notify_CTO_of_high_risk_stories)

      print "Notification complete and log updated."

性能说明

第一次引用每个资产类时都会向服务器发出HTTP请求。

资产不会发出请求,直到需要从它们那里获取数据项。如果之前的请求返回了该属性,则进一步访问属性将被缓存。否则,将发出新的请求。

收集和使用一组资产最快的方法是查询时将您预期要使用的属性包括在选择列表中。如果您手动调用触发完整查询的方法之一,整个结果集将在一个HTTP事务中返回。这些方法包括 __iter__()(例如 .join() 使用此方法),__len__()queryAll()reQueryAll()

写入资产不需要读取它们;设置属性和调用提交函数不会调用“读取”管道。写入资产需要为每个脏资产实例发出一个HTTP POST。

当资产被提交或调用操作时,资产数据将失效,并在下一次属性访问时重新读取。在新鲜查询上调用 queryAll() 并更新您的更新组是提高性能的好方法。

注意:reQueryAll() 将查询对象的脏状态与更新后资产数据失效的方式分开跟踪。除非已更改查询条件,否则 reQueryAll 不会更新缓存数据,并且对于每个失效的数据项访问都会生成新的查询。为了避免这种情况,可以在查询对象上添加和恢复查询条件,以使重新查询实际发生。

reQueryAll() 在实现分页、更改排序等时非常有用,但应谨慎使用。它清除所有缓存数据,因此之前未包含在原始查询中并已检索的字段也会被清除。访问这些字段将触发与之前相同的单个查询。为了避免这个问题,可以在初始查询中包含额外的字段,或者为更新的查询条件创建新的查询对象。

待办事项

  • 使事物具备 Moment 意识

  • 在客户端和服务器之间转换类型(目前一切都是字符串)

  • 添加调试日志

  • 增强测试覆盖率

  • 资产创建模板以及在其他资产上下文中创建

  • 正确处理多值属性,包括删除值。

安装

运行 python setup.py install,或将 v1pysdk 文件夹复制到您的 PYTHONPATH 中。

修订历史

2018-07-02 v0.6.2 - 修复一个关键的缓存问题、错误响应打印、一些HTTP/PUT调用、认证错误处理

由装饰器使用导致的缓存问题阻止了同一类型的多个项目在单个Python解释器调用中更新相同的字段;即,在Python脚本中只能更新一个故事的标题,无论创建了多少个V1Meta对象。它还阻止使用单独的凭据创建V1Meta对象。

处理HTTP 400响应的方式中的错误导致在处理和抛出异常时引发异常,阻止打印HTTP 400提供的实际错误响应。

处理NTLM认证的方式中的错误阻止了HTTP 401认证错误被抛出和处理,因此错误会静默失败,GET/POST命令不会完成。

Python3创建HTTP POST命令时存在一个bug,导致在没有数据负载时抛出TypeError异常。这阻止了在V1对象上使用无参数的操作。

添加了单元测试以确保某些操作正常工作。还添加了连接测试,以确保错误的凭据会导致可识别的失败连接。还添加了特定测试,以确保在同一测试中不同V1Meta对象之间的凭据分离产生不同的结果,从而检查每个V1Meta对象上的缓存是否正常工作。

2018-06-21 v0.6.1 - 修复了一个新项目创建的bug,并为创建添加了单元测试

2018-06-21 v0.6 - 重置以包括0.4和0.5之间丢失的一些历史更改。

修复了测试,使其可以运行并成功,包括添加检查连接功能和一些基本查询功能的测试。

恢复了一些关键丢失的差异:OAuth令牌支持的缓存修复

2018-06-13 v0.5.1 - PyPi上传,可通过pip作为“v1pysdk”使用。

2018-06-12 v0.5 - 添加了对Python3的动态支持。

将page()、sort()、queryAll()、find()、max_length()、length()和len()的使用支持添加到查询对象中。

将主要仓库移动到一个维护的分支。

2013-09-27 v0.4 - 对多值关系设置器代码进行了纠正。它使用了错误的XML "act"属性值,因此多值属性从未正确设置。注意,在此期间,无法从多值关系中取消设置值。

2013-07-09 v0.3 - 为了支持HTTPS,V1Meta和V1Client构造函数中添加了"scheme"参数。

为V1Meta和V1Client添加了一个instance_url关键字参数。此参数可以指定,而不是指定地址、instance_path、scheme和端口参数。

对诸如"list(v1.Story.Name)"之类的调用进行了性能提升。如果请求的属性不存在,则将其添加到选择列表中,从而防止对每个匹配的资产执行HTTP GET。

删除了一些较差的示例,并清理了一些地方的日志。

修复了NTLM和urllib2的一些问题。(感谢campbellr)

缺少的属性现在返回一个类似于None的对象,可以递归地取消引用。 (感谢bazsi)

许可证

重新分发和使用源代码和二进制形式,无论是否修改,只要满足以下条件

  • 源代码的重新分发必须保留上述版权声明、本条件列表和以下免责声明。

  • 二进制形式的重新分发必须以文档和/或其他材料的形式复制上述版权声明、本条件列表和以下免责声明。

  • 未经VersionOne,Inc.的明确事先书面许可,不得使用VersionOne,Inc.的名称或其贡献者的名称来认可或推广由此软件派生的产品。

本软件由版权所有者和贡献者“按原样”提供,并放弃任何明示或暗示的保证,包括但不限于适销性和针对特定目的的适用性的暗示保证。在任何情况下,版权所有者或贡献者均不对任何直接、间接、偶然、特殊、示范性或后果性损害(包括但不限于替代货物或服务的采购;使用、数据或利润的损失;或业务中断)承担责任,无论此类损害是否因本软件的使用或本软件使用的可能性而引起,即使已告知此类损害的可能性。

项目详情


下载文件

下载适用于您平台上的文件。如果您不确定选择哪个,请了解有关安装包的更多信息。

源分布

v1pysdk-0.6.2.tar.gz (41.7 kB 查看哈希值)

上传时间

构建分布

v1pysdk-0.6.2-py3-none-any.whl (26.0 kB 查看哈希值)

上传时间 Python 3

支持者

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面