别院牧志知识库 别院牧志知识库
首页
  • 基础

    • 全栈之路
    • 😎Awesome资源
  • 进阶

    • Python 工匠系列
    • 高阶知识点
  • 指南教程

    • Socket 编程
    • 异步编程
    • PEP 系列
  • 面试

    • Python 面试题
    • 2022 面试记录
    • 2021 面试记录
    • 2020 面试记录
    • 2019 面试记录
    • 数据库索引原理
  • 基金

    • 基金知识
    • 基金经理
  • 细读经典

    • 德隆-三个知道
    • 孔曼子-摊大饼理论
    • 配置者说-躺赢之路
    • 资水-建立自己的投资体系
    • 反脆弱
  • Git 参考手册
  • 提问的智慧
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
首页
  • 基础

    • 全栈之路
    • 😎Awesome资源
  • 进阶

    • Python 工匠系列
    • 高阶知识点
  • 指南教程

    • Socket 编程
    • 异步编程
    • PEP 系列
  • 面试

    • Python 面试题
    • 2022 面试记录
    • 2021 面试记录
    • 2020 面试记录
    • 2019 面试记录
    • 数据库索引原理
  • 基金

    • 基金知识
    • 基金经理
  • 细读经典

    • 德隆-三个知道
    • 孔曼子-摊大饼理论
    • 配置者说-躺赢之路
    • 资水-建立自己的投资体系
    • 反脆弱
  • Git 参考手册
  • 提问的智慧
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 辨析

  • Sockets编程

  • Django

  • stackoverflow

  • Flask

  • 全栈之路

  • 面试

  • 代码片段

  • 异步编程

  • 😎Awesome资源

  • PEP

  • Python工匠系列

  • 高阶知识点

  • Python 学习资源待整理
  • 设计模式

  • 好“艹蛋”的 Python 呀!
  • FIFO | 待学清单📝
  • pip 安装及使用
  • 数据分析

  • 源码阅读计划

    • 源码阅读计划
    • requests 源码阅读
      • 项目结构
      • api 模块
      • sessions
      • models
      • adapters 模块
      • 小技巧
        • json 缩进输出
        • structures
        • status_codes
        • hook
      • 参考链接
    • SQLAlchemy 源码阅读
  • OOP

  • 关于 python 中的 setup.py
  • 并行分布式框架 Celery
  • 七种武器,让你的代码提高可维护性
  • 使用 pdb 调试 Python 代码
  • 每周一个 Python 标准库
  • 🐍Python
  • 源码阅读计划
佚名
2021-06-08
目录

requests 源码阅读

requests 是一个简洁易用的 http-client 库,早期在 github 的 python 项目受欢迎程度可以排名 TOP10。介绍这个项目,我个人觉得还是官方的地道:Requests is an elegant and simple HTTP library for Python,built for human beings. 夸张到是人类就会使用 requests 😃。我们一起阅读一下其源码,学习它是如何实现的。

# 项目结构


本次阅读代码版本是 2.24.0, 从 github 上 clone 项目后,使用 log 命令查看历史信息,找到 tag=2.24.0 的标签,切换版本:

1.  git checkout 0797c61fd541f92f66e409dbf9515ca287af28d2
1

大概浏览一下项目结构和代码,我们可以知道每个模块的功能:

名称 描述
adapters.py 负责 http 连接的处理,主要适配自 urllib3 库
api api 接口
auth http 认证
certs https 证书处理
compat python 版本适配包
cookies cookie 处理
help 帮助
hook 钩子系统
models 数据模型
packages 兼容包相关
sessions session 处理
status_codes http 状态码
structures 数据结构
utils 工具

4000 多行代码,10 多个模块,要全部梳理工作量不小,难度也大。本篇文章我们还是只关注主线,对于支线和细枝末节可以 不求甚解 。

# api 模块

首先还是从 requests 的使用示例出发: shell

r = requests.get('https://api.github.com/user', auth=('user', 'pass'))

r.status_code

200

r.headers['content-type']

'application/json; charset=utf8'

r.encoding

'utf-8'

r.text

'{"type":"User"...'

r.json()

{'private_gists': 419, 'total_private_repos': 77, ...}

上面的使用方法由 api 提供:

1.  # api.py
    
2.    
    
3.  def request(method, url, **kwargs)
    
4.      with sessions.Session() as session:
    
5.          return session.request(method=method, url=url, **kwargs)
    
6.    
    
7.  def get(url, params=None, **kwargs):
    
8.      kwargs.setdefault('allow_redirects', True)
    
9.      return request('get', url, params=params, **kwargs)
    
10.    
    
11.      ...
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这种 get-request 的 api 的封装方式,和我们之前读过的 redis 源码类似,可以让使用者更安全方便。request 具体实现代码是从 session 上下文获取一个 session,然后利用 session.request 发送请求。

同时 api 中还包装了 http 的 OPTIONS, HEAD, POST, PUT, PATCH 和 DELETE 方法。

# sessions

sessions.py 对象的创建和上下文:

1.  # sessions.py
    
2.    
    
3.  class Session(SessionRedirectMixin):
    
4.    
    
5.      def __init__(self):
    
6.          self.headers = default_headers()
    
7.          self.cookies = cookiejar_from_dict({})
    
8.    
    
9.          # Default connection adapters.
    
10.          self.adapters = OrderedDict()
    
11.          ...
    
12.          self.mount('https://', HTTPAdapter())
    
13.    
    
14.      def mount(self, prefix, adapter):
    
15.          self.adapters[prefix] = adapter
    
16.    
    
17.      def __enter__(self):
    
18.          return self
    
19.    
    
20.      def __exit__(self, *args):
    
21.          for v in self.adapters.values():
    
22.              v.close()
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

session 初始化时候,会创建默认的 http-header,http-cookie 信息,建立 HTTPAdpater 对象。__enter__和 __exit__,是上下文装饰器函数,可以用来确保进行 adapter 的 close。

使用 request 方法发送请求:

1.  def request(self, method, url,
    
2.          params=None, data=None, headers=None, cookies=None, files=None,
    
3.          auth=None, timeout=None, allow_redirects=True, proxies=None,
    
4.          hooks=None, stream=None, verify=None, cert=None, json=None):
    
5.      req = Request(
    
6.          method=method.upper(),
    
7.          url=url,
    
8.          headers=headers,
    
9.          files=files,
    
10.          data=data or {},
    
11.          json=json,
    
12.          params=params or {},
    
13.          auth=auth,
    
14.          cookies=cookies,
    
15.          hooks=hooks,
    
16.      )
    
17.      ...
    
18.      prep = PreparedRequest()
    
19.      prep.prepare(
    
20.          method=request.method.upper(),
    
21.          url=request.url,
    
22.          files=request.files,
    
23.          data=request.data,
    
24.          json=request.json,
    
25.          headers=merge_setting(request.headers, self.headers, dict_class=CaseInsensitiveDict),
    
26.          params=merge_setting(request.params, self.params),
    
27.          auth=merge_setting(auth, self.auth),
    
28.          cookies=merged_cookies,
    
29.          hooks=merge_hooks(request.hooks, self.hooks),
    
30.      )
    
31.      ...
    
32.      adapter = self.get_adapter(url=request.url)
    
33.      ...
    
34.      resp = adapter.send(prep, **send_kwargs)
    
35.      return resp
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

request 函数的处理流程,主要分成四步:

  1. 使用请求参数封装 Request 对象

  2. 生成 PreparedRequest 对象,并对 request 对象进行预先处理

  3. 获取对应的 http/https 协议适配器,并用其 send 方法发送请求

  4. 将获取的 Response 对象返回

# models

在进行请求过程中创建了 Request,PreparedRequest 对象,同时从 adpater 中返回了 Response 对象,这 3 个对象的具体实现都在 models.py 模块。

1.  class Request(RequestHooksMixin):
    
2.    
    
3.      def __init__(self,
    
4.              method=None, url=None, headers=None, files=None, data=None,
    
5.              params=None, auth=None, cookies=None, hooks=None, json=None):
    
6.    
    
7.          ...
    
8.          self.hooks = default_hooks()
    
9.          for (k, v) in list(hooks.items()):
    
10.              self.register_hook(event=k, hook=v)
    
11.    
    
12.          self.method = method
    
13.          self.url = url
    
14.          self.headers = headers
    
15.          self.files = files
    
16.          self.data = data
    
17.          self.json = json
    
18.          self.params = params
    
19.          self.auth = auth
    
20.          self.cookies = cookies
    
21.          ...
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

Request 对象创建比较简单,就是做了一些属性的赋值,然后对外部注入的 hook 进行了一下校验,确保是可以执行的函数和函数集合。

1.  def register_hook(self, event, hook):
    
2.      """Properly register a hook."""
    
3.    
    
4.      if event not in self.hooks:
    
5.          raise ValueError('Unsupported event specified, with event name "%s"' % (event))
    
6.    
    
7.      if isinstance(hook, Callable):  ## hook 是一个函数
    
8.          self.hooks[event].append(hook)
    
9.      elif hasattr(hook, '__iter__'):  # hook 也可以是一个迭代器
    
10.          self.hooks[event].extend(h for h in hook if isinstance(h, Callable))
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

PreparedRequest 对象则对外部的参数进行更多的验证和准备:

1.  class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
    
2.    
    
3.      ...
    
4.    
    
5.      def prepare(self,
    
6.          method=None, url=None, headers=None, files=None, data=None,
    
7.          params=None, auth=None, cookies=None, hooks=None, json=None):
    
8.      """Prepares the entire request with the given parameters."""
    
9.    
    
10.          self.prepare_method(method)
    
11.          self.prepare_url(url, params)
    
12.          self.prepare_headers(headers)
    
13.          self.prepare_cookies(cookies)
    
14.          self.prepare_body(data, files, json)
    
15.          self.prepare_auth(auth, url)
    
16.    
    
17.          ...
    
18.          hooks = hooks or []
    
19.          for event in hooks:
    
20.              self.register_hook(event, hooks[event])
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

可以看到 PreparedRequest 对象经过了:

  • 准备 http 方法

  • 准备 url

  • 准备 header

  • 准备 cookie

  • 准备 http-body

  • 准备认证

  • 接受 Request 对象上带来的 hook

hook 我们最后再进行详细介绍,这里以 prepare_headers 为例看看验证过程中都做了什么:

1.  def prepare_headers(self, headers):
    
2.      """Prepares the given HTTP headers."""
    
3.    
    
4.      self.headers = CaseInsensitiveDict()  # 创建字典
    
5.      if headers:
    
6.          for header in headers.items():
    
7.              # Raise exception on invalid header value.
    
8.              check_header_validity(header) # 验证信息
    
9.              name, value = header
    
10.              self.headers[to_native_string(name)] = value  # 赋值
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Response 对象,主要模拟文件操作,raw 保留了二进制数据流,content 属性是获得所有二进制数据,text 属性将二进制数据编码成文本,json 方法则是将文本序列化方法。

1.  CONTENT_CHUNK_SIZE = 10 * 1024 # 10k数据
    
2.    
    
3.  class Response(object):
    
4.    
    
5.      def __init__(self):
    
6.          #: File-like object representation of response (for advanced usage).
    
7.          #: Use of raw requires that stream=True be set on the request.
    
8.          #: This requirement does not apply for use internally to Requests.
    
9.          self.raw = None
    
10.    
    
11.      @property
    
12.      def content(self):
    
13.          """Content of the response, in bytes."""
    
14.          ...
    
15.          self._content = b''.join(self.iter_content(CONTENT_CHUNK_SIZE)) or b''
    
16.          ...
    
17.          return self._content
    
18.    
    
19.      @property
    
20.      def text(self):
    
21.          content = str(self.content, encoding, errors='replace')
    
22.          return content
    
23.    
    
24.      def json(self, **kwargs):
    
25.          ...
    
26.          return complexjson.loads(self.text, **kwargs) 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

requests 优先使用 simplejson 进行 json 的序列化

iter_content 函数中使用一个生成器来迭代的从流中获取数据。至于流如何得到,稍后看 adapter 的实现。

1.  def iter_content(self, chunk_size=1, decode_unicode=False):
    
2.      def generate():
    
3.              # Special case for urllib3.
    
4.              if hasattr(self.raw, 'stream'):
    
5.                  try:
    
6.                      for chunk in self.raw.stream(chunk_size, decode_content=True):
    
7.                          yield chunk
    
8.      stream_chunks = generate()
    
9.      return stream_chunks
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# adapters 模块

具体的 http 请求如何发送的呢?主要就在 HTTPAdapter 中了:

1.  class HTTPAdapter(BaseAdapter):
    
2.      def __init__(self, pool_connections=DEFAULT_POOLSIZE,
    
3.                   pool_maxsize=DEFAULT_POOLSIZE, max_retries=DEFAULT_RETRIES,
    
4.                   pool_block=DEFAULT_POOLBLOCK):
    
5.          ...
    
6.          # 初始化连接池
    
7.          self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize,
    
8.                                         block=block, strict=True, **pool_kwargs)
    
9.    
    
10.      def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
    
11.          conn = self.poolmanager.connection_from_url(url) # 获取连接
    
12.    
    
13.          url = self.request_url(request, proxies)
    
14.          self.add_headers(request, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies)
    
15.    
    
16.          # 发送请求
    
17.          resp = conn.urlopen(
    
18.                      method=request.method,
    
19.                      url=url,
    
20.                      body=request.body,
    
21.                      headers=request.headers,
    
22.                      redirect=False,
    
23.                      assert_same_host=False,
    
24.                      preload_content=False,
    
25.                      decode_content=False,
    
26.                      retries=self.max_retries,
    
27.                      timeout=timeout
    
28.                  )
    
29.    
    
30.          return self.build_response(request, resp)
    
31.    
    
32.      def close(self):
    
33.          self.poolmanager.clear()  # 连接池关闭
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

这里主要用了urllib3库提供的 PoolManager 和 urlopen,本篇文章我们就不深入里面的实现了,重点看看如何生成 Response 对象:

1.  def build_response(self, req, resp):
    
2.      response = Response()
    
3.    
    
4.      # Fallback to None if there's no status_code, for whatever reason.
    
5.      response.status_code = getattr(resp, 'status', None)
    
6.    
    
7.      # Make headers case-insensitive.
    
8.      response.headers = CaseInsensitiveDict(getattr(resp, 'headers', {}))
    
9.    
    
10.      # Set encoding.
    
11.      response.encoding = get_encoding_from_headers(response.headers)
    
12.      response.raw = resp  # 二进制流
    
13.      response.reason = response.raw.reason
    
14.    
    
15.      if isinstance(req.url, bytes):
    
16.          response.url = req.url.decode('utf-8')
    
17.      else:
    
18.          response.url = req.url
    
19.    
    
20.      # Add new cookies from the server.
    
21.      extract_cookies_to_jar(response.cookies, req, resp)
    
22.    
    
23.      # Give the Response some context.
    
24.      response.request = req
    
25.      response.connection = self
    
26.    
    
27.      return response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  • resp 是 urllib3 的 HTTPResponse 实现

  • cookie 是合并了 Request 和 Response

  • Response 还引用了 PreparedRequest 对象,可以让 response 的使用更方便

使用 requests 进行 http 请求的过程,主要集中在上面四个模块,现在对其核心过程都有了一定的了解。https 则是在 http 基础上,做了更多的验证等工作。可以简单回顾一下请求执行流程:

  1. api 中封装易用的 API

  2. Session 中进行流程的处理

  3. Request 和 PreparedRequest 对请求进行预处理

  4. Response 对响应进行封装,提供更易用的方法(json)和数据(ok)

# 小技巧

requests 库中还有一些代码,也让使用更简单,可以借鉴。

# json 缩进输出

json 输出的时候定义 indent 参数可以进行缩进,sort_keys可以进行排序。

1.  # help.py
    
2.    
    
3.  """Pretty-print the bug information as JSON."""
    
4.  print(json.dumps(info(), sort_keys=True, indent=2))
1
2
3
4
5
6
7

下面是示例和展示:

1.  a = {
    
2.          "name": "game404",
    
3.          "age": 2
    
4.      }
    
5.  print(json.dumps(a)) 
    
6.  print(json.dumps(a, sort_keys=True, indent=2))  # 定义indent参数
    
7.  # 输出
    
8.  {"name": "game404", "age": 2}
    
9.  {
    
10.    "age": 2,
    
11.    "name": "game404"
    
12.  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# structures

structures 模块中定义了 2 个数据结构。普通的 python 字典不可以使用 . 取值, 如果需要使用 . 需要定义对象:

1.  # structures.py
    
2.    
    
3.  a = {
    
4.      "name":"game404"
    
5.  }
    
6.  # print(a.name)  # AttributeError
    
7.  print(a["name"])
    
8.    
    
9.  # 定义一个数据结构对象
    
10.  class Person(object):
    
11.    
    
12.      def __init__(self, name):
    
13.          self.name = name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

LookupDict 可以不用定义对象属性又使用 . 取值,这在一些配置类上会很方便:

1.  class LookupDict(dict):
    
2.      """Dictionary lookup object."""
    
3.    
    
4.      def __init__(self, name=None):
    
5.          self.name = name
    
6.          super(LookupDict, self).__init__()
    
7.    
    
8.      def __repr__(self):
    
9.          return '<lookup \'%s\'>' % (self.name)
    
10.    
    
11.      def __getitem__(self, key):
    
12.          # We allow fall-through here, so values default to None
    
13.          # 可以使用. 取值的魔法函数
    
14.          return self.__dict__.get(key, None)
    
15.    
    
16.      def get(self, key, default=None):
    
17.          return self.__dict__.get(key, default
    
18.    
    
19.  a = LookupDict(name="game404")
    
20.  a["motto"] = "Life is short, you need Python"
    
21.  a.age = 2
    
22.  print(a["motto"], a.age, a["age"])  # none, 2, 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

CaseInsensitiveDict 定义了大小写不敏感的字典,用来处理 http-header:

1.  class CaseInsensitiveDict(MutableMapping):
    
2.    
    
3.      def __init__(self, data=None, **kwargs):
    
4.          self._store = OrderedDict()  # 使用额外的_store存储数据
    
5.          if data is None:
    
6.              data = {}
    
7.          self.update(data, **kwargs)
    
8.    
    
9.      def __setitem__(self, key, value):
    
10.          # Use the lowercased key for lookups, but store the actual
    
11.          # key alongside the value.
    
12.          self._store[key.lower()] = (key, value)  # 字典的key都转换为小写
    
13.    
    
14.      def __delitem__(self, key):
    
15.          del self._store[key.lower()]
    
16.    
    
17.  cid = CaseInsensitiveDict()
    
18.  cid['Accept'] = 'application/json'
    
19.  print(cid['aCCEPT'] == 'application/json')  # True
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

可以看到 CaseInsensitiveDict 对象的dict实际上使用_store 包装了一层:

  1. print(cid.dict) # {'_store': OrderedDict([('accept', ('Accept', 'application/json'))])}

  2. print(cid._store) # OrderedDict([('accept', ('Accept', 'application/json'))])

# status_codes

status_codes 中定义了 http 状态码的语义化名称,比如 OK 是 200 的语义化表达,不懂 http 的人也可以看到 ok 状态。

1.  print(requests.codes["ok"], requests.codes.OK, requests.codes.ok, requests.codes.OKAY)  #200 200 200 200
    
2.  print(requests.codes.CREATED)  # 201
    
3.  print(requests.codes.found)  # 302
    
1
2
3
4
5
6

其实现方法主要是:

1.  # statuc_codes.py
    
2.    
    
3.  codes = LookupDict(name='status_codes')
    
4.  for code, titles in _codes.items():
    
5.          for title in titles:
    
6.              setattr(codes, title, code)  # 默认key
    
7.              if not title.startswith(('\\', '/')):
    
8.                  setattr(codes, title.upper(), code)  # 大写key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# hook

hooks 提供了一个简单的钩子系统,可以对一个事件名称注册多个处理函数(前面的 register_hook),然后在合适的时候触发就可以获取对数据进行处理, 数据处理过程类似 linux 的管道符号 | :

1.  # hooks.py
    
2.    
    
3.  HOOKS = ['response']
    
4.    
    
5.    
    
6.  def default_hooks():  # 初始化默认的事件
    
7.      return {event: [] for event in HOOKS}
    
8.    
    
9.  def dispatch_hook(key, hooks, hook_data, **kwargs):
    
10.      """Dispatches a hook dictionary on a given piece of data."""
    
11.      hooks = hooks or {}
    
12.      hooks = hooks.get(key)
    
13.      if hooks:
    
14.          if hasattr(hooks, '__call__'):  # 判断是函数还是函数集合
    
15.              hooks = [hooks]
    
16.          for hook in hooks:
    
17.              _hook_data = hook(hook_data, **kwargs)  # 注意hook会返回数据,由下一个函数继续处理
    
18.              if _hook_data is not None:
    
19.                  hook_data = _hook_data
    
20.      return hook_data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

使用方法在:

1.  class Session(SessionRedirectMixin):
    
2.    
    
3.      def send(self, request, **kwargs):
    
4.          ...
    
5.          r = adapter.send(request, **kwargs)
    
6.          # Response manipulation hooks
    
7.          r = dispatch_hook('response', hooks, r, **kwargs)
1
2
3
4
5
6
7
8
9
10
11
12
13

session 在获取到请求后,触发预先定义的钩子,对 response 进行进一步的处理。

# 参考链接

  • https://requests.readthedocs.io/zh_CN/latest/

  • https://urllib3.readthedocs.io/en/latest/

  • https://gist.github.com/kennethreitz42/973705

编辑 (opens new window)
#requests#源码阅读
上次更新: 2024-07-15, 08:03:22
源码阅读计划
SQLAlchemy 源码阅读

← 源码阅读计划 SQLAlchemy 源码阅读→

最近更新
01
提升沟通亲和力的实用策略
03-26
02
工作
07-15
03
如何选房子
06-25
更多文章>
Theme by Vdoing | Copyright © 2019-2025 IMOYAO | 别院牧志
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式