65.9K
CodeProject 正在变化。 阅读更多。
Home

CodeProject API 的 Python 包装器

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (3投票s)

2016 年 1 月 1 日

CPOL

23分钟阅读

viewsIcon

21569

downloadIcon

123

本文介绍了用 Python 编写的 CodeProject API 封装器的工作原理和用法。

引言

在本文中,我将解释我为 CodeProject API[^] 构建的 Python 封装器的工作原理。我构建它的目的是让您可以通过几个方法调用,轻松地从 Python 与 CodeProject API 进行交互,而无需自己编写 API 交互代码。我将介绍如何使用该封装器,以及它在底层的工作原理。该封装器可用于 Python 2 和 3。

CPApiWrapper 类

该封装器包含在 CPApiWrapper 类中,该类包含所有从 API 获取数据和访问令牌的方法。该类有一个成员变量:access_token_data。这包含访问令牌和一些相关信息(见下一节)。构造函数将此成员的初始值设置为 None

def __init__(self):
    self.access_token_data = None

类的使用和构造函数

from CPApiWrapper.cpapiwrapper import CPApiWrapper
# ^ assumes that the Python package is in a subdirectory called "CPApiWrapper"
wrapper = CPApiWrapper()

导入语句

值得一看的是 CPApiWrapper.py 顶部的导入语句

import requests
from .exceptions import *
from .itemsummarylistviewmodel import ItemSummaryListViewModel
from .accesstokendata import AccessTokenData
from .mynotificationsviewmodel import MyNotificationsViewModel
from .myprofileviewmodel import MyProfileViewModel
from .myreputationviewmodel import MyReputationViewModel
from .forumdisplaymode import ForumDisplayMode
from .questionlistmode import QuestionListMode
try:
    from urlparse import urljoin  # Python 2
except ImportError:
    from urllib.parse import urljoin  # Python 3

第一个导入没什么好说的,它只是导入了 requests 包,它是封装器的依赖项。之后的导入以点开头。这种导入是一种包相对导入:它导入相对于当前包(在本例中为 CPApiWrapper 包)的内容。

最后一个导入导入了 urljoin 方法。此方法在 Python 2 和 Python 3 中位于两个不同的包中:Python 2 中位于 urlparse,Python 3 中位于 urllib.parse。为了支持这两个版本,导入被包装在一个 try-except 语句中:如果 Python 2 导入失败,它将执行 Python 3 导入。然后,您可以在代码中的任何地方调用 urljoin,而无需担心版本问题。

获取访问令牌

您必须先获取访问令牌才能发送任何请求。

获取访问令牌的方法名为 get_access_token,它需要两个辅助类:AccessTokenDataAccessTokenNotFetchedException

AccessTokenData 类

这个类用于保存从 API 返回的访问令牌数据。它有三个类成员

  • access_token - 访问令牌,以字符串形式存储。
  • expires_in - 告知令牌何时过期。
  • token_type - OAuth 令牌类型。

get_access_token 类将使用这个类来存储 API 响应。构造函数有一个 data 参数。创建 AccessTokenData 实例时,您必须将一个字典作为此参数传递,其中包含所需字段。当此类从 get_access_token 调用时,它将把解析后的 JSON 响应传递给构造函数。

class AccessTokenData():
    def __init__(self, data):
        self.access_token = data["access_token"]
        self.expires_in = data["expires_in"]
        self.token_type = data["token_type"]

AccessTokenNotFetchedException

get_access_token 未能获取 API 令牌时,会抛出此类型的异常。它继承自 Exception。它有一个 parsed_json 成员变量,get_access_token 在您捕获异常并想知道 API 响应了什么时,会在此处存储解析后的 API 响应。在 AccessTokenNotFetchedException 构造函数的开头,它调用其超类的 __init__ 方法,并以 message 作为参数,以确保传递的消息确实被设置为异常消息。

class AccessTokenNotFetchedException(Exception):
    def __init__(self, message, parsed_json):
        super(AccessTokenNotFetchedException, self).__init__(message)
        self.parsed_json = parsed_json

get_access_token 方法

这是实际获取访问令牌的方法。获取后,它会将 AccessTokenData 实例存储在 access_token_data 成员中。

要从 API 获取访问令牌,您必须提供您的客户端 ID、客户端密钥以及可选的 CodeProject 登录电子邮件和密码。要获取客户端 ID 和客户端密钥,您必须在 CodeProject Web API 客户端设置中注册您的应用程序。传递电子邮件和密码是可选的:API 仅在您需要访问包含您的声誉、文章、问题等信息的“我的 API”时才要求提供。

该方法将在 POST 请求中将提供的客户端 ID、客户端密钥(以及可选的电子邮件和密码)传递给 https://api.codeproject.com/Token。传递给 API 的数据是 grant_type=client_credentials&client_id=ID&client_secret=secret(不带电子邮件/密码)和 grant_type=password&client_id=ID&client_secret=secret&username=email&password=password(带电子邮件和密码)。我们无需自行编码数据。我们使用 requests 库为我们完成此操作:我们只需将包含数据的字典传递给 requests.postrequests 将负责正确的编码。API 返回包含我们将提供给 AccessTokenData 类的数据的 JSON。如果 JSON 响应具有 "error" 字段,则会抛出 AccessTokenNotFethchedException

def get_access_token(self, client_id, client_secret,
                     email=None, password=None):
    if email is None or password is None:
        data = {"grant_type": "client_credentials",
                "client_id": client_id,
                "client_secret": client_secret}
    else:
        data = {"grant_type": "password",
                "client_id": client_id,
                "client_secret": client_secret,
                "username": email,
                "password": password}
    resp = requests.post("https://api.codeproject.com/Token", data).json()
    if "error" in resp:
        exc = AccessTokenNotFetchedException(
            "Could not fetch API Access Token. Error: " + resp["error"],
            resp
        )
        raise exc
    self.access_token_data = AccessTokenData(resp)

发送 API 请求

CPApiWrapper 类有许多方法可以通过一个方法调用获取特定的 API 数据——例如,获取最新问题的第 1 页。所有这些方法都使用一个辅助方法来发送给定 URL 的实际 API 请求,即 api_request。它接受一个 url 参数和一个 params 参数(默认为 None),用于将查询字符串参数作为字典传递。url 参数必须是相对于 https://api.codeproject.com/v1 的 URL。api_request 使用 urljoin 方法将 URL 连接起来,我们使用 requests 包发送带有提供参数的 GET 请求。如果 API 返回的 JSON 响应中包含“error”或“message”字段,则表示出现问题,该方法将抛出 ApiRequestFailedException

ApiRequestFailedException 类的外观和工作方式与上面讨论的 AccessTokenNotFetchedException 类完全相同。它只是名称不同。

class ApiRequestFailedException(Exception):
    def __init__(self, message, parsed_json):
        super(ApiRequestFailedException, self).__init__(message)
        self.parsed_json = parsed_json

这是 api_request 方法

def api_request(self, url, params=None):
    if self.access_token_data is None:
        exc = AccessTokenNotFetchedException(
            "Fetch the API key before sending a request.",
            None
        )
        raise exc
    url = url.lstrip("/")
    url = urljoin("https://api.codeproject.com/v1/", url)
    headers = {'Pragma': 'No-cache',
               'Accept': 'application/json',
               'Authorization': '{0} {1}'.format(
                   self.access_token_data.token_type,
                   self.access_token_data.access_token
               )}
    if params is not None:
        passed_params = {k: v for (k, v) in params.items() if v is not None}
    else:
        passed_params = None
    resp = requests.get(url, headers=headers, params=passed_params)
    data = resp.json()
    if "message" in data:
        exc = ApiRequestFailedException(
            "API Request Failed. Message: " + data["message"],
            data
        )
        raise exc
    if "error" in data:
        exc = ApiRequestFailedException(
            "API Request Failed. Message: " + data["error"],
            data
        )
        raise exc
    return data

如上代码块所示,我们使用 lstrip 从传入的 url 参数中移除前导斜杠。这是必须的,因为如果相对 API URL 以斜杠开头,封装器会认为它是完全有效的,但如果保留前导斜杠,urljoin 会使相对 URL 相对于目录 https://api.codeproject.com/,而不是相对于 https://api.codeproject.com/v1/

params 不会直接传递给 requests.get:这个字典中的某些值可能为 None,因此不应传递它们。我们使用字典推导过滤掉它们。所有值为非 None 的键值对都将被保留。.items() 在 Python 2 和 Python 3 中具有不同的返回类型,但两者都适用于字典推导。

视图模型

如果您打算使用封装器,您可能不需要上面描述的 api_request 方法。CPApiWrapper 类中有许多方法使用 api_request 方法发送请求,并将响应存储在视图模型中。

这是视图模型的简要概述

  • NameIdPair - 名称和 ID 的容器。
  • ItemSummaryListViewModel - PaginationInfo 对象和 ItemSummary 列表的容器。由多个 API 页面返回。
  • ItemSummary - 包含有关“项目”的数据,可以是问题、论坛消息、文章等。
  • PaginationInfo - 包含有关分页的信息:当前页码、总页数等。
  • MyNotificationsViewModel - NotificationViewModel 对象的容器。
  • NotificationViewModel - 包含有关通知的数据的容器。
  • MyProfileViewModel - 包含您的个人资料数据,例如声誉、显示名称等。
  • MyReputationViewModel - 包含您的总声誉以及您在每个类别中的声誉。
  • ReputationTypeViewModel - 包含有关特定声誉类别的数据:名称、您在该类别中的积分数量等。

NameIdPair

这个视图模型在其他视图模型中非常常用。它有两个成员:nameid。与所有视图模型一样,NameIdPair 有一个接受字典的构造函数,该字典必须是解析后的 API 响应的一部分(或对于某些视图模型,是整个解析后的响应)。

def __init__(self, data):
    self.name = data['name']
    self.id = data['id']

有时,API 响应的一部分不仅仅是一个 NameIdPair,而是列表中的多个。这种情况经常发生,因此 NameIdPair 提供了一个静态方法 data_to_pairs,它将解析后的 API 响应中的字典列表转换为 NameIdPair 类的实例列表。这被放在一个单独的方法中,因为如果由于某种原因需要更改它,就不必在发生此转换的每个地方进行更改,而只需在一个地方进行更改:该方法。

@staticmethod
def data_to_pairs(data):
    return [NameIdPair(x) for x in data]

此方法使用列表推导将字典列表转换为 NameIdPairs 列表:对于 data 中的每个 x,它都采用 NameIdPair(x)

ItemSummaryListViewModel

ItemSummaryListViewModel 由最新文章、问题和消息的 API 页面返回。它具有以下类成员

  • pagination - PaginationInfo 的实例,顾名思义,它包含有关分页的信息。
  • items - ItemSummary 数组。

与所有视图模型一样,ItemSumamryListViewModel 有一个接受字典的构造函数,该字典必须是解析后的 API 响应。

from .paginationinfo import PaginationInfo
from .itemsummary import ItemSummary


class ItemSummaryListViewModel():
    def __init__(self, data):
        self.pagination = PaginationInfo(data['pagination'])
        self.items = ItemSummary.data_to_items(data['items'])

data['pagination'] 也是一个字典,所以我们将其传递给 PaginationInfo 构造函数。data['items'] 是一个字典列表。ItemSummarydata_to_items 方法遍历此列表中的所有项目,并返回一个 ItemSummary 列表(参见下一节)。

ItemSummary

此视图模型包含“项目”的摘要:文章/博客/提示、问题、答案或消息。它具有以下成员(其中一些是可选的。如果它们是可选的且在 API 响应中不存在,则其值将为 None

  • id - 项目的 ID。
  • title - 项目的标题。
  • authors - 作者(以及文章的潜在合著者)的 NameIdPair 列表。
  • summary - 可选。项目的摘要。如果是文章/博客/提示,则包含描述(实际上可以为空,因此此成员的值可以为 None,但这不会经常发生),否则(问题/答案/消息)包含前几行。
  • content_type - 类似于 doc_type(见下文),但它对问题和答案也返回 Article因为 CodeProject 的系统将文章用于问题和答案[^]
  • doc_type - 带有“文档类型”名称和 ID 的 NameIdPair,可以是以下任意一种
    doc_type 的名称/ID 对
    注意:这些文档类型不区分大小写。
    ID 名称
    1 文章
    2 技术博客
    3 提示/技巧
    4 问题
    5 答案
    6 论坛消息
    13 reference
  • categories - 项目类别的 NameIdPair 列表。
  • tags - 项目标签的 NameIdPair 列表。
  • license - 可选。项目许可证的 NameIdPair
  • created_date - 显示项目创建的 UTC 日期
  • modified_date - 显示项目上次修改的 UTC 日期。如果尚未修改,则显示创建日期。
  • thread_editor - 可选。如果适用,包含讨论线程最新编辑者的 NameIdPair
  • thread_modification_date - 可选。如果适用,包含讨论线程的最新修改日期。
  • rating - 项目的评分
  • votes - 项目的投票数量
  • popularity - 项目的受欢迎程度。来自 热门文章页面: “受欢迎程度衡量文章被阅读后产生的兴趣。它计算为 评分 x Log10(# 投票),其中评分为过滤后的评分。有关详细信息,请参阅 FAQ。”
  • website_link - 指向网站上项目的链接
  • api_link - 尚未实现[^]
  • parent_id - 项目父项的 ID。如果它没有父项,则为 0
  • thread_id - 讨论线程的 ID。如果项目不是论坛消息,则此值为 0
  • indent_level - 论坛消息的“缩进级别”:线程为 0,回复为 1,回复的回复为 2 等。如果项目不是论坛消息,则此值为 0

这是 ItemSummary 类的构造函数

def __init__(self, data):
    self.id = data['id']
    self.title = data['title']
    self.authors = NameIdPair.data_to_pairs(data['authors'])
    self.summary = data['summary']
    self.content_type = data['contentType']
    self.doc_type = NameIdPair(data['docType'])
    self.categories = NameIdPair.data_to_pairs(data['categories'])
    self.tags = NameIdPair.data_to_pairs(data['tags'])
    if 'license' in data and data['license'] is not None and \
            data['license']['name'] is not None:
        self.license = NameIdPair(data['license'])
    else:
        self.license = None
    self.created_date = data['createdDate']
    self.modified_date = data['modifiedDate']
    if 'threadEditor' in data and data['threadEditor'] is not None\
            and data['threadEditor']['name'] is not None:
        self.thread_editor = NameIdPair(data['threadEditor'])
    else:
        self.thread_editor = None
    if 'threadModifiedDate' in data:
        self.thread_modified_date = data['threadModifiedDate']
    else:
        self.thread_modified_date = None
    self.rating = data['rating']
    self.votes = data['votes']
    self.popularity = data['popularity']
    self.website_link = data['websiteLink']
    self.api_link = data['apiLink']
    self.parent_id = data['parentId']
    self.thread_id = data['threadId']
    self.indent_level = data['indentLevel']

构造函数用 API 的响应填充所有成员,并用 None 填充以下字段(如果它们不存在):licensethread_editorthread_modified_date。请注意,其中一些字段可能存在于 API 响应中,但仍然表示它不存在。例如,如果 API 响应中的 license{"id": 0, "name": null},则表示没有许可证。我的 API 封装器仍然将其标记为 None

该类还有一个静态 data_to_items 方法,用于将 API 响应中的 ItemSummary 列表转换为 ItemSummary 类的实例。此方法在 ItemSummaryListViewModel 构造函数中使用。它的工作方式与 NameIdPairdata_to_pairs 方法相同。

@staticmethod
def data_to_items(data):
    return [ItemSummary(x) for x in data]

PaginationInfo

PaginationInfoItemSummaryListViewModel 使用,它表示分页。它具有以下成员

  • page - 当前页
  • page_size - 每页大小
  • total_pages - 总页数
  • total_items - 总项目数

构造函数没有什么特别之处,它只是用解析后的 API 响应中的数据填充实例的成员。

class PaginationInfo():
    def __init__(self, data):
        self.page = data['page']
        self.page_size = data['pageSize']
        self.total_pages = data['totalPages']
        self.total_items = data['totalItems']

MyNotificationsViewModel

MyNotificationsViewModelMy API 的 notifications 页面返回。它有一个成员:notifications,它是一个 NotificationViewModel 列表。

from .notificationviewmodel import NotificationViewModel


class MyNotificationsViewModel():
    def __init__(self, data):
        self.notifications = NotificationViewModel.data_to_notifications(
            data['notifications']
        )

NotificationViewModel.data_to_notifications 是一个方法,它将解析后的 JSON 字典列表转换为 NotificationViewModel 列表。

NotificationViewModel

此视图模型存储有关通知的信息,例如当有人回答您的问题、回复您的消息、评论您的文章时,您会收到通知......

NotificationViewModel 具有以下成员

  • id - 通知的 ID。
  • object_type_name, object_id, topic - 我不完全确定这些代表什么。[^]
  • subject - 通知的标题。
  • notification_date - 您收到通知的日期。
  • unread - 指示通知是否未读的布尔值。
  • content - 通知的内容。
  • link - 通知链接。

如上一节所述,NotificationViewModel 类有一个静态 data_to_notifications 方法。它的工作方式与 NameIdPair 中的 data_to_pairs 相同,只是类不同。

class NotificationViewModel():
    def __init__(self, data):
        self.id = data['id']
        self.object_type_name = data['objectTypeName']
        self.object_id = data['objectId']
        self.subject = data['subject']
        self.topic = data['topic']
        self.notification_date = data['notificationDate']
        self.unread = data['unRead']
        self.content = data['content']
        self.link = data['link']

    @staticmethod
    def data_to_notifications(data):
        return [NotificationViewModel(x) for x in data]

MyProfileViewModel

此视图模型包含您个人资料的所有信息,包括公共和私有信息。请注意,您不必担心有人会使用 API 窃取您的私人数据:我的 API 仅在您登录后才可访问,并且您只能使用它来获取的信息,而不是其他人的。如果您在网站上未填写可选值,API 将为此项返回一个空字符串。该视图模型具有以下成员

  • id - 包含用户 ID。注意:这与 codeproject_member_id 不同(见下文)。id用于跨站点识别的值[^]。
  • user_name - 您的用户名。用于 https://codeproject.org.cn/Members/User-Name 的那个
  • display_name - 您的显示名称,显示在您的问答帖子底部、消息旁边等。
  • avatar - 您的个人资料图片 URL。
  • email - 您的电子邮件地址。
  • html_emails - 指示您是否接受 HTML 电子邮件的布尔值。
  • country - 您的国家/地区。
  • home_page - 您的主页 URL。
  • codeproject_member_id - 您在 CodeProject 上的会员 ID,您可以在 https://codeproject.org.cn/script/Membership/View.aspx?mid=<member ID> 中找到
  • member_profile_page_url - 您的个人资料页面 URL。
  • twitter_name - 您在 Twitter 上的名称。
  • google_plus_profile - 您的 Google+ 个人资料 ID。
  • linkedin_profile - 您的 LinkedIn 个人资料 ID。
  • biography - 您提供的简历。
  • company - 您工作的公司。
  • job_title - 您的职位。

MyProfileViewModel 的构造函数与任何其他视图模型的构造函数相同:它用解析后的 JSON 字典中的数据填充上述成员。

class MyProfileViewModel():
    def __init__(self, data):
        self.id = data['id']
        self.user_name = data['userName']
        self.display_name = data['displayName']
        self.avatar = data['avatar']
        self.email = data['email']
        self.html_emails = data['htmlEmails']
        self.country = data['country']
        self.home_page = data['homePage']
        self.codeproject_member_id = data['codeProjectMemberId']
        self.member_profile_page_url = data['memberProfilePageUrl']
        self.twitter_name = data['twitterName']
        self.google_plus_profile = data['googlePlusProfile']
        self.linkedin_profile = data['linkedInProfile']
        self.biography = data['biography']
        self.company = data['company']
        self.job_title = data['jobTitle']

MyReputationViewModel

此视图模型包含您在 CodeProject 上的声誉信息。它具有以下成员

  • total_points - 您的总声誉积分。
  • reputation_types - 一个 ReputationTypeViewModel 列表(见下一节),用于将总声誉积分划分为不同的部分。
  • graph_url - 您的声誉图表的 URL。
from .reputationtypeviewmodel import ReputationTypeViewModel


class MyReputationViewModel():
    def __init__(self, data):
        self.total_points = data['totalPoints']
        self.reputation_types = ReputationTypeViewModel.data_to_types(
            data['reputationTypes']
        )
        self.graph_url = data['graphUrl']

ReputationTypeViewModel

此视图模型包含一种声誉类型的声誉积分数量。它在 MyReputationViewModel 中使用。它具有以下成员

  • name - 声誉类型的名称。
  • points - 您为此声誉类型拥有的积分数量。
  • designation - 与名称和级别一起出现的声誉级别的名称(见下文)。例如,如果您是白金作者,则为 Legend
  • level - 您在该声誉类型中的称号,例如 silver

ReputationTypeViewModel 还有一个静态 data_to_types 方法,用于将解析后的 JSON 字典列表转换为 ReputationTypeViewModel 列表。它的工作方式与 NameIdPairdata_to_pairs 完全相同。

class ReputationTypeViewModel():
    def __init__(self, data):
        self.name = data['name']
        self.points = data['points']
        self.level = data['level']
        self.designation = data['designation']

    @staticmethod
    def data_to_types(data):
        return [ReputationTypeViewModel(x) for x in data]

类方法从 API 获取数据并将其存储在视图模型中

我们现在已经了解了所有视图模型。但它们有什么用呢?在 CPApiWrapper 类中,有许多方法使用 api_request 方法并将其响应存储在视图模型中。这些方法的目的是让您的生活更轻松,因为您不必自己使用 api_request 方法。

所有这些方法都是实例方法。您可以在创建封装器实例并获取访问令牌后调用它们。示例

from CPApiWrapper.cpapiwrapper import CPApiWrapper
# ^ assumes that the Python package is in a subdirectory called "CPApiWrapper"
wrapper = CPApiWrapper()
wrapper.get_access_token("Client ID here", "Client Secret here")  # optionally email and password
latest_articles = wrapper.get_articles()

最新文章:get_articles

此方法使用 Articles API 获取最新文章列表。它返回一个 ItemSummaryListViewModel 并具有以下可选参数

  • tags - 期望一个逗号分隔的标签列表。只有包含这些标签的文章才会返回。默认值为 None(即所有标签)。
  • min_rating - 返回文章的最低评分。默认值为 3.0
  • page - 要显示的页面。默认值为 1
def get_articles(self, tags=None, min_rating=3.0, page=1):
    data = self.api_request("/Articles", {"tags": tags,
                                          "minRating": min_rating,
                                          "page": page})
    return ItemSummaryListViewModel(data)

获取论坛消息:get_messages_from_forum

get_messages_from_forum 方法调用 /Forum/{forum ID}/{mode} 来获取论坛中的最新消息。该方法具有以下参数

  • forum_id - 您要获取消息的论坛 ID。
  • mode - 论坛的显示模式,可以是消息或主题。请参阅下文了解更多信息。默认是主题。
  • page - 要显示的页面。默认值为 1

mode 有两个有效值:"Messages""Threads"。您可以将这些字符串字面量中的任何一个作为参数传递给 get_messages_from_forum 方法,但您也可以使用 ForumDisplayMode 类的成员。该类有两个成员:membersthreads。我建议使用该类的成员:如果 mode 的有效值发生变化,只需在一个地方进行更改。

def get_messages_from_forum(self, forum_id, mode=ForumDisplayMode.threads,
                            page=1):
    data = self.api_request("/Forum/{0}/{1}".format(forum_id, mode),
                            {"page": page})
    return ItemSummaryListViewModel(data)

获取线程中的消息:get_messages_from_thread

您还可以使用 get_messages_from_thread 方法获取特定线程中的论坛消息。它调用 /MessageThread/{id}/messages 来完成此操作,并将响应存储在 ItemSummaryViewModel 中。该方法具有以下参数

  • thread_id - 消息线程的 ID
  • page - 要显示的页面。默认值为 1
def get_messages_from_thread(self, thread_id, page=1):
    data = self.api_request(
        "/MessageThread/{0}/messages".format(thread_id),
        {"page": page}
    )
    return ItemSummaryListViewModel(data)

最新问题:get_questions

get_questions 方法获取最新活动/新/未回答的问题,并返回一个 ItemSummaryViewModel。它接受以下参数

  • mode - 指定您要最新问题、最新活动问题还是最新未回答问题。默认值为 QuestionListMode.active
  • include - 问题必须包含的逗号分隔标签。默认值为 None(即所有标签)
  • ignore - 问题不得包含的逗号分隔标签。默认值为 None(即无忽略标签)
  • page - 要显示的页面。默认值为 1

QuestionListMode 是一个类似于 ForumDisplayMode 的类。它有三个成员:activenewunanswered

def get_questions(self, mode=QuestionListMode.active, include=None,
                  ignore=None, page=1):
    data = self.api_request("/Questions/{0}".format(mode),
                            {"include": include,
                             "ignore": ignore,
                             "page": page})
    return ItemSummaryListViewModel(data)

我的 API

访问我的 API 的方法是最简单和最相似的方法。它们仅返回类型不同,其中一些带有 page 参数,另一些则没有。此参数始终是可选的,默认值为 1

  • get_my_answers - 使用 /my/answers 获取您的最新答案。返回一个 ItemSummaryViewModel。有一个可选的 page 参数。
  • get_my_articles - 使用 /my/articles 获取您的文章。返回一个 ItemSummaryViewModel。有一个可选的 page 参数。
  • get_my_blog_posts - 使用 /my/blogposts 获取您的博客文章。返回一个 ItemSummaryViewModel。有一个可选的 page 参数。
  • get_my_bookmarks - 使用 /my/bookmarks 获取您收藏的项目。返回一个 ItemSummaryViewModel。有一个可选的 page 参数。
  • get_my_messages - 使用 /my/messages 获取您的最新论坛消息。返回一个 ItemSummaryViewModel。有一个可选的 page 参数。
  • get_my_notifications - 使用 /my/notifications 获取您的未读通知。返回一个 MyNotificationsViewModel
  • get_my_profile - 使用 /my/profile 获取您的个人资料信息。返回一个 MyProfileViewModel
  • get_my_questions - 使用 /my/questions 获取您的最新问题。返回一个 ItemSummaryViewModel。有一个可选的 page 参数。
  • get_my_reputation - 使用 /my/reputation 获取您的声誉积分。返回一个 MyReputationViewModel
  • get_my_tips - 使用 /my/tips 获取您的提示。返回一个 ItemSummaryViewModel。有一个可选的 page 参数。
def get_my_answers(self, page=1):
    data = self.api_request("/my/answers", {"page": page})
    return ItemSummaryListViewModel(data)

def get_my_articles(self, page=1):
    data = self.api_request("/my/articles", {"page": page})
    return ItemSummaryListViewModel(data)

def get_my_blog_posts(self, page=1):
    data = self.api_request("/my/blogposts", {"page": page})
    return ItemSummaryListViewModel(data)

def get_my_bookmarks(self, page=1):
    data = self.api_request("/my/bookmarks", {"page": page})
    return ItemSummaryListViewModel(data)

def get_my_messages(self, page=1):
    data = self.api_request("/my/messages", {"page": page})
    return ItemSummaryListViewModel(data)

def get_my_notifications(self):
    data = self.api_request("/my/notifications")
    return MyNotificationsViewModel(data)

def get_my_profile(self):
    data = self.api_request("/my/profile")
    return MyProfileViewModel(data)

def get_my_questions(self, page=1):
    data = self.api_request("/my/questions", {"page": page})
    return ItemSummaryListViewModel(data)

def get_my_reputation(self):
    data = self.api_request("/my/reputation")
    return MyReputationViewModel(data)

def get_my_tips(self, page=1):
    data = self.api_request("/my/tips", {"page": page})
    return ItemSummaryListViewModel(data)

测试用例

所有方法和视图模型都已创建。为了确保没有错误,我们需要有测试用例。由于 API 响应是动态的,我们不能有检查静态输出的测试用例。相反,测试检查我们是否从 API 获取到完整数据(即视图模型是否正确填充),在不应抛出异常的地方没有异常,以及在预期时是否抛出异常。

测试用例使用两个封装器:一个未经电子邮件/密码认证,一个经过认证。两者都应该适用于非“我的 API”,只有第二个应该适用于“我的 API”。

测试用例可以在 test_apiwrapper.py 中找到。该文件开头有几个方法,用于检查视图模型是否完全填充(可选成员除外,如果有的话)。它们都遵循相同的模式:首先检查视图模型实例是否不为 None,然后检查所有非视图模型属性是否为 None,并且所有视图模型属性是否完整。这意味着这些断言方法可能会调用其他断言方法。

断言方法的一个例子,assert_nameidpair_is_complete

def assert_nameidpair_is_complete(value):
    assert value is not None
    assert value.name is not None
    assert value.id is not None

它首先断言 value(传递的 NameIdPair 实例)不为 None,然后对所有成员执行相同的操作。如果任何断言失败,将抛出 AssertionError

我不会将其他断言方法复制到文章中:它们都看起来相同,唯一的区别是有些视图模型具有可选成员。

文章封装器方法测试:test_get_articles

在断言方法之后,您可以找到用于验证封装器特定部分是否正常工作的方法。这些方法都具有相同的方法签名:它们都接受一个参数,该参数应该是 CPApiWrapper 的实例。

test_get_articles 方法首先验证 CPApiWrapperget_articles 方法在没有传递任何参数(即使用所有默认参数值)时是否返回正确的数据。它首先检查返回的 ItemSummaryViewModel 是否完整,然后检查返回的页面是否确实是第 1 页,并且所有文章的评分都在 3.0 或以上。

articles = w.get_articles()
assert_itemsummarylistviewmodel_is_complete(articles)
assert articles.pagination.page == 1
for article in articles.items:
    assert article.rating >= 3.0

test_get_articles 的下一个测试将标签列表、最低评分和页码传递给 get_articles。收到响应后,它会检查返回的 ItemSummaryViewModel 是否完整,以及所有文章是否符合指定的标准。

articles = w.get_articles("C#,C++", min_rating=4.0, page=3)
assert_itemsummarylistviewmodel_is_complete(articles)
assert articles.pagination.page == 3
for article in articles.items:
    assert article.rating >= 4.0
    tags_upper = [tag.name.upper() for tag in article.tags]
    assert "C#" in tags_upper or "C++" in tags_upper

test_get_articles 的最后一个测试验证了 min_rating 也适用于浮点数。4.5 作为 min_rating 参数传递给 get-articles,其他参数是默认值。

articles = w.get_articles(min_rating=4.5)
assert_itemsummarylistviewmodel_is_complete(articles)
assert articles.pagination.page == 1
for article in articles.items:
    assert article.rating >= 4.5

问题封装器测试:test_get_questions

下一个测试方法是 test_get_questions,用于测试 CPApiWrapperget_questions 方法。它首先遍历所有 QuestionListMode,并使用当前模式和所有标签发送页面 1 问题的 API 请求。它断言返回的 ItemSummaryViewModel 完整且确实获取了页面 1。

for m in [QuestionListMode.unanswered, QuestionListMode.active,
          QuestionListMode.new]:
    questions = w.get_questions(m)
    assert_itemsummarylistviewmodel_is_complete(questions)
    assert questions.pagination.page == 1

之后,该方法测试 get_questionsinclude 参数是否有效。在收到 ItemSummaryViewModel 后,它检查所有问题是否具有所需标签之一。测试用例使用 HTMLCSS 作为包含标签。

questions = w.get_questions(include="HTML,CSS")
assert_itemsummarylistviewmodel_is_complete(questions)
for question in questions.items:
    tags_upper = [tag.name.upper() for tag in question.tags]
    assert "HTML" in tags_upper or "CSS" in tags_upper

检查包含标签后,接着检查被忽略的标签。这部分代码与包含标签的检查非常相似,只是我们现在检查问题是否未被标记为任何被忽略的标签。

questions = w.get_questions(ignore="C#,SQL")
assert_itemsummarylistviewmodel_is_complete(questions)
for question in questions.items:
    tags_upper = [tag.name.upper() for tag in question.tags]
    assert "C#" not in tags_upper and "SQL" not in tags_upper

该方法的最后一部分是检查 page 参数是否正常工作。在 get_questions 返回 ItemSummaryViewModel 后,该方法断言它已完成,并且当前页面是 2,与传递给 get_questions 的值相同。

questions = w.get_questions(page=2)
assert_itemsummarylistviewmodel_is_complete(questions)
assert questions.pagination.page == 2

测试论坛消息封装器方法:test_get_forum_messages

测试 get_forum_messages 的方法非常简单:它遍历所有 ForumDisplayMode,从 ID 为 1159 的论坛(Lounge[^])加载当前模式下的最新消息,并断言返回的 ItemSummaryViewModel 是完整的。

def test_get_forum_messages(w):
    for m in [ForumDisplayMode.messages, ForumDisplayMode.threads]:
        messages = w.get_messages_from_forum(1159, m)
        assert_itemsummarylistviewmodel_is_complete(messages)

测试消息线程封装器方法:test_get_thread_messages

此方法测试 CPApiWrapperget_messages_from_thread 方法。它以 消息 5058566[^] 作为线程调用该方法,并断言返回的 ItemSummaryViewModel 是完整的。

def test_get_thread_messages(w):
    messages = w.get_messages_from_thread(5058566)
    assert_itemsummarylistviewmodel_is_complete(messages)

测试我的 API 封装器方法:test_my

“我的 API”的所有不同封装器方法都以相同的方式进行测试:调用方法,并断言返回的视图模型是完整的。对于带有可选页面参数的方法,我们进行两次调用:一次使用 page = 1(默认值),一次使用 page = 2。

然而,对于大多数视图模型,在断言它们完整之前,我们必须首先检查 <视图模型实例>.items 的长度是否大于零,如果不是,则不要尝试断言视图模型完整,因为我们将收到断言错误。我们之前的测试方法不需要这样做,但在这里必须这样做,因为无法确定您是否在网站上发布了文章、答案、问题等,如果您没有,.items 将为空,这在这里是完全有效的情况。

def test_my(w):
    answers = w.get_my_answers()
    if len(answers.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(answers)
        assert answers.pagination.page == 1

    answers = w.get_my_answers(page=2)
    if len(answers.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(answers)
        assert answers.pagination.page == 2

    articles = w.get_my_articles()
    if len(articles.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(articles)
        assert articles.pagination.page == 1

    articles = w.get_my_articles(page=2)
    if len(articles.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(articles)
        assert articles.pagination.page == 2

    blog = w.get_my_blog_posts()
    if len(blog.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(blog)
        assert blog.pagination.page == 1

    blog = w.get_my_blog_posts(page=2)
    if len(blog.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(blog)
        assert blog.pagination.page == 2

    bookmarks = w.get_my_bookmarks()
    if len(bookmarks.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(bookmarks)
        assert bookmarks.pagination.page == 1

    bookmarks = w.get_my_bookmarks(page=2)
    if len(bookmarks.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(bookmarks)
        assert bookmarks.pagination.page == 2

    messages = w.get_my_messages()
    if len(messages.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(messages)
        assert messages.pagination.page == 1

    messages = w.get_my_messages(page=2)
    if len(messages.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(messages)
        assert messages.pagination.page == 2

    notifications = w.get_my_notifications()
    assert_mynotificationsviewmodel_is_complete(notifications)

    profile = w.get_my_profile()
    assert_myprofileviewmodel_is_complete(profile)

    questions = w.get_my_questions()
    if len(questions.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(questions)
        assert questions.pagination.page == 1

    questions = w.get_my_questions(page=2)
    if len(questions.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(questions)
        assert questions.pagination.page == 2

    rep = w.get_my_reputation()
    assert_myrepviewmodel_is_complete(rep)

    tips = w.get_my_tips()
    if len(tips.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(tips)
        assert tips.pagination.page == 1

    tips = w.get_my_tips(page=2)
    if len(tips.items) > 0:
        assert_itemsummarylistviewmodel_is_complete(tips)
        assert tips.pagination.page == 2

执行测试用例

我们现在已经看完了所有的测试方法,但是也应该有执行这些测试的代码,这些代码可以在 test_my 方法之后找到。首先要做的,是向用户询问客户端 ID、客户端密钥、CodeProject 登录电子邮件和密码。这有两种选择:将所有内容作为命令行参数提供,或者通过标准输入提供所有内容。参数可以在 sys.argv 变量中找到(需要 import sys)。如果此列表的长度为 4,则使用命令行参数,否则,向用户请求输入。

if len(sys.argv) == 4:
    client_id = sys.argv[0]
    client_secret = sys.argv[1]
    email = sys.argv[2]
    password = sys.argv[3]
else:
    try:
        input_ = raw_input  # Python 2
    except NameError:
        input_ = input  # Python 3
    client_id = input_("Client ID: ")
    client_secret = input_("Client Secret: ")
    email = input_("Email: ")
    password = getpass("Password: ")

封装器适用于 Python 2 和 Python 3,因此测试用例也应适用于这两个版本。这里我们看到了一个区别:在 Python 2 中,raw_input 应该用于提示客户端 ID/密钥和电子邮件(Python 2 的 input 基本上是 eval(raw_input())),但在 Python 3 中,raw_input 不再存在,而被称为 input。我们将正确的输入方法存储为 input_。然后我们为客户端 ID、客户端密钥和登录电子邮件调用此函数。但是对于密码,我们使用 getpass(需要 from getpass import getpass),它会隐藏用户输入,适用于密码。

然后,封装器被初始化。初始化后,我们断言两个封装器的 access_token_data 成员都为 None。然后我们尝试使用其中一个封装器获取最新文章。这必须失败并引发 AccessTokenNotFetchedException,因为封装器尚未获取访问令牌。

wrapper1 = CPApiWrapper()
wrapper2 = CPApiWrapper()
assert wrapper1.access_token_data is None
assert wrapper2.access_token_data is None
try:
    wrapper1.get_articles()
    assert False  # If the API key is not fetched, it should raise an exception
except AccessTokenNotFetchedException:
    pass  # Correct behavior

之后,获取访问令牌。wrapper1 不传递电子邮件和密码,而 wrapper2 传递。

print("Fetching access token")
wrapper1.get_access_token(client_id, client_secret)
wrapper2.get_access_token(client_id, client_secret, email, password)

然后是时候运行两个封装器的 test_... 方法了。我们使用 for 循环遍历它们,并将当前的一个作为参数传递给测试方法。请注意,test_my 在 for 循环外部调用:它只应针对 wrapper 2 进行测试。

i = 0
for wr in [wrapper1, wrapper2]:
    i += 1
    print("Testing for wrapper {0}".format(i))
    print("Testing /Articles")
    test_get_articles(wr)
    print("Testing /Questions")
    test_get_questions(wr)
    print("Testing /Forum")
    test_get_forum_messages(wr)
    print("Testing /MessageThread")
    test_get_thread_messages(wr)
print("Testing /my")
test_my(wrapper2)

我们还需要测试,如果我们尝试使用 wrapper 1 访问 My API,封装器是否会抛出异常:我们没有在那里提供电子邮件和密码。

print("Testing that /my throws an error without email/password")
try:
    wrapper1.api_request("/my/articles")
    assert False
except ApiRequestFailedException:
    pass

在测试文件末尾,如果所有测试都成功通过,则有一个打印语句,打印 Test passed!

print("Tests passed!")

现在我们已经了解了 CodeProject API 封装器的工作原理。我希望这篇文章和封装器能对您有所帮助。感谢阅读!

© . All rights reserved.