hs-net 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hs_net-0.1.0/.github/workflows/ci.yml +91 -0
- hs_net-0.1.0/.github/workflows/publish.yml +81 -0
- hs_net-0.1.0/.gitignore +29 -0
- hs_net-0.1.0/.pre-commit-config.yaml +18 -0
- hs_net-0.1.0/.python-version +1 -0
- hs_net-0.1.0/LICENSE +21 -0
- hs_net-0.1.0/PKG-INFO +200 -0
- hs_net-0.1.0/README.md +164 -0
- hs_net-0.1.0/docs/.gitignore +14 -0
- hs_net-0.1.0/docs/README.md +29 -0
- hs_net-0.1.0/docs/docs/_nav.json +17 -0
- hs_net-0.1.0/docs/docs/api/_meta.json +1 -0
- hs_net-0.1.0/docs/docs/api/config.mdx +94 -0
- hs_net-0.1.0/docs/docs/api/exceptions.mdx +101 -0
- hs_net-0.1.0/docs/docs/api/net.mdx +140 -0
- hs_net-0.1.0/docs/docs/api/response.mdx +117 -0
- hs_net-0.1.0/docs/docs/api/sync-net.mdx +70 -0
- hs_net-0.1.0/docs/docs/guide/_meta.json +1 -0
- hs_net-0.1.0/docs/docs/guide/configuration.mdx +142 -0
- hs_net-0.1.0/docs/docs/guide/getting-started.mdx +104 -0
- hs_net-0.1.0/docs/docs/guide/introduction.mdx +61 -0
- hs_net-0.1.0/docs/docs/index.md +37 -0
- hs_net-0.1.0/docs/docs/public/rspress-dark-logo.png +0 -0
- hs_net-0.1.0/docs/docs/public/rspress-icon.png +0 -0
- hs_net-0.1.0/docs/docs/public/rspress-light-logo.png +0 -0
- hs_net-0.1.0/docs/docs/usage/_meta.json +1 -0
- hs_net-0.1.0/docs/docs/usage/advanced.mdx +138 -0
- hs_net-0.1.0/docs/docs/usage/basic-request.mdx +92 -0
- hs_net-0.1.0/docs/docs/usage/error-handling.mdx +95 -0
- hs_net-0.1.0/docs/docs/usage/http-methods.mdx +104 -0
- hs_net-0.1.0/docs/docs/usage/middleware.mdx +170 -0
- hs_net-0.1.0/docs/docs/usage/multi-engine.mdx +104 -0
- hs_net-0.1.0/docs/docs/usage/selectors.mdx +190 -0
- hs_net-0.1.0/docs/package.json +21 -0
- hs_net-0.1.0/docs/pnpm-lock.yaml +2426 -0
- hs_net-0.1.0/docs/rspress.config.ts +29 -0
- hs_net-0.1.0/docs/tsconfig.json +28 -0
- hs_net-0.1.0/examples/01_basic_get.py +54 -0
- hs_net-0.1.0/examples/02_http_methods.py +56 -0
- hs_net-0.1.0/examples/03_selectors.py +134 -0
- hs_net-0.1.0/examples/04_middleware.py +128 -0
- hs_net-0.1.0/examples/05_multi_engine.py +108 -0
- hs_net-0.1.0/examples/06_advanced.py +176 -0
- hs_net-0.1.0/examples/07_real_world.py +147 -0
- hs_net-0.1.0/pyproject.toml +75 -0
- hs_net-0.1.0/src/hs_net/__init__.py +34 -0
- hs_net-0.1.0/src/hs_net/_request_builder.py +90 -0
- hs_net-0.1.0/src/hs_net/client.py +415 -0
- hs_net-0.1.0/src/hs_net/config.py +52 -0
- hs_net-0.1.0/src/hs_net/engines/__init__.py +3 -0
- hs_net-0.1.0/src/hs_net/engines/aiohttp_engine.py +109 -0
- hs_net-0.1.0/src/hs_net/engines/base.py +148 -0
- hs_net-0.1.0/src/hs_net/engines/curl_cffi_engine.py +193 -0
- hs_net-0.1.0/src/hs_net/engines/httpx_engine.py +193 -0
- hs_net-0.1.0/src/hs_net/engines/requests_engine.py +103 -0
- hs_net-0.1.0/src/hs_net/engines/requests_go_engine.py +187 -0
- hs_net-0.1.0/src/hs_net/exceptions.py +103 -0
- hs_net-0.1.0/src/hs_net/models.py +62 -0
- hs_net-0.1.0/src/hs_net/py.typed +0 -0
- hs_net-0.1.0/src/hs_net/response/__init__.py +4 -0
- hs_net-0.1.0/src/hs_net/response/response.py +164 -0
- hs_net-0.1.0/src/hs_net/response/selector.py +92 -0
- hs_net-0.1.0/src/hs_net/signals.py +108 -0
- hs_net-0.1.0/src/hs_net/sync_client.py +407 -0
- hs_net-0.1.0/src/hs_net/ua.py +21 -0
- hs_net-0.1.0/tests/__init__.py +0 -0
- hs_net-0.1.0/tests/conftest.py +69 -0
- hs_net-0.1.0/tests/test_client_integration.py +224 -0
- hs_net-0.1.0/tests/test_config.py +69 -0
- hs_net-0.1.0/tests/test_engines.py +93 -0
- hs_net-0.1.0/tests/test_exceptions.py +82 -0
- hs_net-0.1.0/tests/test_models.py +80 -0
- hs_net-0.1.0/tests/test_request_builder.py +133 -0
- hs_net-0.1.0/tests/test_response.py +199 -0
- hs_net-0.1.0/tests/test_signals.py +117 -0
- hs_net-0.1.0/tests/test_ua.py +51 -0
- hs_net-0.1.0/uv.lock +2172 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [dev]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
lint:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python
|
|
20
|
+
run: uv python install 3.13
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: uv sync --group dev
|
|
24
|
+
|
|
25
|
+
- name: Ruff lint
|
|
26
|
+
run: uv run ruff check src/
|
|
27
|
+
|
|
28
|
+
- name: Ruff format check
|
|
29
|
+
run: uv run ruff format --check src/
|
|
30
|
+
|
|
31
|
+
- name: Bandit security check
|
|
32
|
+
run: uv run bandit -c pyproject.toml -r src/
|
|
33
|
+
|
|
34
|
+
test:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
strategy:
|
|
37
|
+
matrix:
|
|
38
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
|
|
42
|
+
- name: Install uv
|
|
43
|
+
uses: astral-sh/setup-uv@v4
|
|
44
|
+
|
|
45
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
46
|
+
run: uv python install ${{ matrix.python-version }}
|
|
47
|
+
|
|
48
|
+
- name: Install dependencies
|
|
49
|
+
run: uv sync --group dev
|
|
50
|
+
|
|
51
|
+
- name: Run tests
|
|
52
|
+
run: uv run pytest tests/ -v --ignore=tests/test_client_integration.py
|
|
53
|
+
|
|
54
|
+
integration-test:
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
needs: [lint, test]
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
|
|
60
|
+
- name: Install uv
|
|
61
|
+
uses: astral-sh/setup-uv@v4
|
|
62
|
+
|
|
63
|
+
- name: Set up Python
|
|
64
|
+
run: uv python install 3.13
|
|
65
|
+
|
|
66
|
+
- name: Install dependencies
|
|
67
|
+
run: uv sync --group dev
|
|
68
|
+
|
|
69
|
+
- name: Run integration tests
|
|
70
|
+
run: uv run pytest tests/test_client_integration.py -v
|
|
71
|
+
|
|
72
|
+
build:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
needs: [lint, test]
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
|
|
78
|
+
- name: Install uv
|
|
79
|
+
uses: astral-sh/setup-uv@v4
|
|
80
|
+
|
|
81
|
+
- name: Set up Python
|
|
82
|
+
run: uv python install 3.13
|
|
83
|
+
|
|
84
|
+
- name: Build package
|
|
85
|
+
run: uv build
|
|
86
|
+
|
|
87
|
+
- name: Upload artifacts
|
|
88
|
+
uses: actions/upload-artifact@v4
|
|
89
|
+
with:
|
|
90
|
+
name: dist
|
|
91
|
+
path: dist/
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
check:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
outputs:
|
|
12
|
+
should_publish: ${{ steps.check_version.outputs.should_publish }}
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0
|
|
17
|
+
|
|
18
|
+
- name: Check if version changed
|
|
19
|
+
id: check_version
|
|
20
|
+
run: |
|
|
21
|
+
# 获取当前版本
|
|
22
|
+
CURRENT_VERSION=$(grep -oP 'version = "\K[^"]+' pyproject.toml | head -1)
|
|
23
|
+
echo "Current version: $CURRENT_VERSION"
|
|
24
|
+
|
|
25
|
+
# 检查该版本 tag 是否已存在
|
|
26
|
+
if git tag -l "v$CURRENT_VERSION" | grep -q .; then
|
|
27
|
+
echo "Version v$CURRENT_VERSION already published, skipping."
|
|
28
|
+
echo "should_publish=false" >> $GITHUB_OUTPUT
|
|
29
|
+
else
|
|
30
|
+
echo "New version v$CURRENT_VERSION detected."
|
|
31
|
+
echo "should_publish=true" >> $GITHUB_OUTPUT
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
ci:
|
|
35
|
+
needs: check
|
|
36
|
+
if: needs.check.outputs.should_publish == 'true'
|
|
37
|
+
uses: ./.github/workflows/ci.yml
|
|
38
|
+
|
|
39
|
+
publish:
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
needs: [check, ci]
|
|
42
|
+
if: needs.check.outputs.should_publish == 'true'
|
|
43
|
+
permissions:
|
|
44
|
+
id-token: write
|
|
45
|
+
contents: write
|
|
46
|
+
environment: pypi
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- name: Install uv
|
|
51
|
+
uses: astral-sh/setup-uv@v4
|
|
52
|
+
|
|
53
|
+
- name: Set up Python
|
|
54
|
+
run: uv python install 3.13
|
|
55
|
+
|
|
56
|
+
- name: Build package
|
|
57
|
+
run: uv build
|
|
58
|
+
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
run: uv publish
|
|
61
|
+
env:
|
|
62
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
63
|
+
|
|
64
|
+
- name: Get version
|
|
65
|
+
id: version
|
|
66
|
+
run: |
|
|
67
|
+
VERSION=$(grep -oP 'version = "\K[^"]+' pyproject.toml | head -1)
|
|
68
|
+
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
69
|
+
|
|
70
|
+
- name: Create Git tag
|
|
71
|
+
run: |
|
|
72
|
+
git tag "v${{ steps.version.outputs.version }}"
|
|
73
|
+
git push origin "v${{ steps.version.outputs.version }}"
|
|
74
|
+
|
|
75
|
+
- name: Create GitHub Release
|
|
76
|
+
uses: softprops/action-gh-release@v2
|
|
77
|
+
with:
|
|
78
|
+
tag_name: "v${{ steps.version.outputs.version }}"
|
|
79
|
+
name: "v${{ steps.version.outputs.version }}"
|
|
80
|
+
files: dist/*
|
|
81
|
+
generate_release_notes: true
|
hs_net-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# IDE
|
|
13
|
+
.idea/
|
|
14
|
+
.vscode/
|
|
15
|
+
*.swp
|
|
16
|
+
*.swo
|
|
17
|
+
|
|
18
|
+
# Testing
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# Docs
|
|
24
|
+
docs/node_modules/
|
|
25
|
+
docs/doc_build/
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
# bandit - 安全检查
|
|
3
|
+
- repo: https://github.com/PyCQA/bandit
|
|
4
|
+
rev: 1.7.10
|
|
5
|
+
hooks:
|
|
6
|
+
- id: bandit
|
|
7
|
+
args: ["-c", "pyproject.toml"]
|
|
8
|
+
exclude: ^tests/
|
|
9
|
+
additional_dependencies: ["bandit[toml]"]
|
|
10
|
+
|
|
11
|
+
# ruff - lint + format
|
|
12
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
13
|
+
rev: 'v0.6.9'
|
|
14
|
+
hooks:
|
|
15
|
+
- id: ruff
|
|
16
|
+
args: [--fix, --exit-non-zero-on-fix, --show-fixes]
|
|
17
|
+
- id: ruff-format
|
|
18
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
hs_net-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 昊色居士
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hs_net-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hs-net
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 统一多引擎的增强型 HTTP 客户端,内置重试、选择器、信号中间件
|
|
5
|
+
Project-URL: Homepage, https://github.com/x-haose/hs-net
|
|
6
|
+
Project-URL: Repository, https://github.com/x-haose/hs-net
|
|
7
|
+
Project-URL: Issues, https://github.com/x-haose/hs-net/issues
|
|
8
|
+
Author-email: 昊色居士 <x-haose@users.noreply.github.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: aiohttp,async,http,httpx,network,scraping
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: aiohttp>=3.9
|
|
24
|
+
Requires-Dist: blinker>=1.8
|
|
25
|
+
Requires-Dist: curl-cffi>=0.7
|
|
26
|
+
Requires-Dist: fake-useragent>=1.5
|
|
27
|
+
Requires-Dist: furl>=2.1
|
|
28
|
+
Requires-Dist: httpx[http2,socks]>=0.28
|
|
29
|
+
Requires-Dist: jmespath>=1.1.0
|
|
30
|
+
Requires-Dist: parsel>=1.9
|
|
31
|
+
Requires-Dist: pydantic>=2.9
|
|
32
|
+
Requires-Dist: requests-go>=1.0
|
|
33
|
+
Requires-Dist: requests>=2.31
|
|
34
|
+
Requires-Dist: tenacity>=8.3
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# hs-net
|
|
38
|
+
|
|
39
|
+
统一多引擎的增强型 HTTP 客户端,内置重试、选择器、信号中间件,同步异步全支持。
|
|
40
|
+
|
|
41
|
+
## 特性
|
|
42
|
+
|
|
43
|
+
- **多引擎切换** — httpx、aiohttp、curl-cffi、requests、requests-go,统一 API,一行切换
|
|
44
|
+
- **同步 & 异步** — `Net`(异步)和 `SyncNet`(同步),接口完全一致
|
|
45
|
+
- **智能选择器** — 内置 CSS、XPath、正则、JMESPath 四种数据提取
|
|
46
|
+
- **自动重试** — 基于 tenacity,可配置次数、间隔、随机抖动
|
|
47
|
+
- **信号中间件** — 请求前、响应后、重试时三个钩子
|
|
48
|
+
- **反爬支持** — curl-cffi 浏览器 TLS 指纹模拟 + 随机 User-Agent
|
|
49
|
+
|
|
50
|
+
## 安装
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install hs-net
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> Python >= 3.10,所有引擎开箱即用,无需额外安装。
|
|
57
|
+
|
|
58
|
+
## 快速开始
|
|
59
|
+
|
|
60
|
+
### 异步
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import asyncio
|
|
64
|
+
from hs_net import Net
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
async with Net() as net:
|
|
68
|
+
resp = await net.get("https://example.com")
|
|
69
|
+
print(resp.css("title::text").get()) # Example Domain
|
|
70
|
+
|
|
71
|
+
asyncio.run(main())
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 同步
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from hs_net import SyncNet
|
|
78
|
+
|
|
79
|
+
with SyncNet() as net:
|
|
80
|
+
resp = net.get("https://example.com")
|
|
81
|
+
print(resp.css("title::text").get()) # Example Domain
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 引擎切换
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# httpx(默认)
|
|
88
|
+
Net(engine="httpx")
|
|
89
|
+
|
|
90
|
+
# aiohttp
|
|
91
|
+
Net(engine="aiohttp")
|
|
92
|
+
|
|
93
|
+
# curl-cffi(支持浏览器指纹模拟)
|
|
94
|
+
Net(engine="curl_cffi", engine_options={"impersonate": "chrome120"})
|
|
95
|
+
|
|
96
|
+
# requests(仅同步)
|
|
97
|
+
SyncNet(engine="requests")
|
|
98
|
+
|
|
99
|
+
# requests-go
|
|
100
|
+
Net(engine="requests_go")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 数据提取
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
resp = await net.get("https://example.com")
|
|
107
|
+
|
|
108
|
+
# CSS 选择器
|
|
109
|
+
resp.css("title::text").get()
|
|
110
|
+
|
|
111
|
+
# XPath
|
|
112
|
+
resp.xpath("//h1/text()").get()
|
|
113
|
+
|
|
114
|
+
# 正则
|
|
115
|
+
resp.re_first(r"价格: (\d+)元")
|
|
116
|
+
|
|
117
|
+
# JMESPath(JSON 响应)
|
|
118
|
+
resp = await net.get("https://api.example.com/users")
|
|
119
|
+
resp.jmespath("data[?age > `18`].name")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 配置
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from hs_net import Net, NetConfig
|
|
126
|
+
|
|
127
|
+
# 方式 1:构造函数参数
|
|
128
|
+
net = Net(
|
|
129
|
+
engine="httpx",
|
|
130
|
+
base_url="https://api.example.com/v1",
|
|
131
|
+
timeout=30.0,
|
|
132
|
+
retries=5,
|
|
133
|
+
retry_delay=1.0,
|
|
134
|
+
user_agent="chrome",
|
|
135
|
+
verify=False,
|
|
136
|
+
concurrency=10,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# 方式 2:NetConfig 对象
|
|
140
|
+
config = NetConfig(
|
|
141
|
+
engine="curl_cffi",
|
|
142
|
+
retries=3,
|
|
143
|
+
user_agent="random",
|
|
144
|
+
engine_options={"impersonate": "chrome120"},
|
|
145
|
+
)
|
|
146
|
+
net = Net(config=config)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
参数优先级:`请求方法参数 > 构造函数参数 > NetConfig 默认值`
|
|
150
|
+
|
|
151
|
+
## 信号中间件
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
async with Net() as net:
|
|
155
|
+
|
|
156
|
+
@net.on_request_before
|
|
157
|
+
async def add_auth(req_data):
|
|
158
|
+
req_data.headers["Authorization"] = "Bearer token"
|
|
159
|
+
return req_data
|
|
160
|
+
|
|
161
|
+
@net.on_response_after
|
|
162
|
+
async def log_response(resp):
|
|
163
|
+
print(f"{resp.status_code} {resp.url}")
|
|
164
|
+
|
|
165
|
+
@net.on_request_retry
|
|
166
|
+
async def on_retry(exc):
|
|
167
|
+
print(f"重试: {exc}")
|
|
168
|
+
|
|
169
|
+
resp = await net.get("https://example.com")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 错误处理
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from hs_net import Net, StatusException, RetryExhausted, RequestException
|
|
176
|
+
|
|
177
|
+
async with Net() as net:
|
|
178
|
+
try:
|
|
179
|
+
resp = await net.get("https://httpbin.org/status/404")
|
|
180
|
+
except RetryExhausted as e:
|
|
181
|
+
print(f"{e.attempts} 次重试失败: {e.last_exception}")
|
|
182
|
+
except StatusException as e:
|
|
183
|
+
print(f"HTTP {e.code}")
|
|
184
|
+
except RequestException as e:
|
|
185
|
+
print(f"请求异常: {e}")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 文档
|
|
189
|
+
|
|
190
|
+
完整文档见 `docs/` 目录,本地预览:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
cd docs
|
|
194
|
+
pnpm install
|
|
195
|
+
pnpm run dev
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
hs_net-0.1.0/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# hs-net
|
|
2
|
+
|
|
3
|
+
统一多引擎的增强型 HTTP 客户端,内置重试、选择器、信号中间件,同步异步全支持。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **多引擎切换** — httpx、aiohttp、curl-cffi、requests、requests-go,统一 API,一行切换
|
|
8
|
+
- **同步 & 异步** — `Net`(异步)和 `SyncNet`(同步),接口完全一致
|
|
9
|
+
- **智能选择器** — 内置 CSS、XPath、正则、JMESPath 四种数据提取
|
|
10
|
+
- **自动重试** — 基于 tenacity,可配置次数、间隔、随机抖动
|
|
11
|
+
- **信号中间件** — 请求前、响应后、重试时三个钩子
|
|
12
|
+
- **反爬支持** — curl-cffi 浏览器 TLS 指纹模拟 + 随机 User-Agent
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install hs-net
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> Python >= 3.10,所有引擎开箱即用,无需额外安装。
|
|
21
|
+
|
|
22
|
+
## 快速开始
|
|
23
|
+
|
|
24
|
+
### 异步
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import asyncio
|
|
28
|
+
from hs_net import Net
|
|
29
|
+
|
|
30
|
+
async def main():
|
|
31
|
+
async with Net() as net:
|
|
32
|
+
resp = await net.get("https://example.com")
|
|
33
|
+
print(resp.css("title::text").get()) # Example Domain
|
|
34
|
+
|
|
35
|
+
asyncio.run(main())
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 同步
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from hs_net import SyncNet
|
|
42
|
+
|
|
43
|
+
with SyncNet() as net:
|
|
44
|
+
resp = net.get("https://example.com")
|
|
45
|
+
print(resp.css("title::text").get()) # Example Domain
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 引擎切换
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# httpx(默认)
|
|
52
|
+
Net(engine="httpx")
|
|
53
|
+
|
|
54
|
+
# aiohttp
|
|
55
|
+
Net(engine="aiohttp")
|
|
56
|
+
|
|
57
|
+
# curl-cffi(支持浏览器指纹模拟)
|
|
58
|
+
Net(engine="curl_cffi", engine_options={"impersonate": "chrome120"})
|
|
59
|
+
|
|
60
|
+
# requests(仅同步)
|
|
61
|
+
SyncNet(engine="requests")
|
|
62
|
+
|
|
63
|
+
# requests-go
|
|
64
|
+
Net(engine="requests_go")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 数据提取
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
resp = await net.get("https://example.com")
|
|
71
|
+
|
|
72
|
+
# CSS 选择器
|
|
73
|
+
resp.css("title::text").get()
|
|
74
|
+
|
|
75
|
+
# XPath
|
|
76
|
+
resp.xpath("//h1/text()").get()
|
|
77
|
+
|
|
78
|
+
# 正则
|
|
79
|
+
resp.re_first(r"价格: (\d+)元")
|
|
80
|
+
|
|
81
|
+
# JMESPath(JSON 响应)
|
|
82
|
+
resp = await net.get("https://api.example.com/users")
|
|
83
|
+
resp.jmespath("data[?age > `18`].name")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 配置
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from hs_net import Net, NetConfig
|
|
90
|
+
|
|
91
|
+
# 方式 1:构造函数参数
|
|
92
|
+
net = Net(
|
|
93
|
+
engine="httpx",
|
|
94
|
+
base_url="https://api.example.com/v1",
|
|
95
|
+
timeout=30.0,
|
|
96
|
+
retries=5,
|
|
97
|
+
retry_delay=1.0,
|
|
98
|
+
user_agent="chrome",
|
|
99
|
+
verify=False,
|
|
100
|
+
concurrency=10,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# 方式 2:NetConfig 对象
|
|
104
|
+
config = NetConfig(
|
|
105
|
+
engine="curl_cffi",
|
|
106
|
+
retries=3,
|
|
107
|
+
user_agent="random",
|
|
108
|
+
engine_options={"impersonate": "chrome120"},
|
|
109
|
+
)
|
|
110
|
+
net = Net(config=config)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
参数优先级:`请求方法参数 > 构造函数参数 > NetConfig 默认值`
|
|
114
|
+
|
|
115
|
+
## 信号中间件
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
async with Net() as net:
|
|
119
|
+
|
|
120
|
+
@net.on_request_before
|
|
121
|
+
async def add_auth(req_data):
|
|
122
|
+
req_data.headers["Authorization"] = "Bearer token"
|
|
123
|
+
return req_data
|
|
124
|
+
|
|
125
|
+
@net.on_response_after
|
|
126
|
+
async def log_response(resp):
|
|
127
|
+
print(f"{resp.status_code} {resp.url}")
|
|
128
|
+
|
|
129
|
+
@net.on_request_retry
|
|
130
|
+
async def on_retry(exc):
|
|
131
|
+
print(f"重试: {exc}")
|
|
132
|
+
|
|
133
|
+
resp = await net.get("https://example.com")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## 错误处理
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from hs_net import Net, StatusException, RetryExhausted, RequestException
|
|
140
|
+
|
|
141
|
+
async with Net() as net:
|
|
142
|
+
try:
|
|
143
|
+
resp = await net.get("https://httpbin.org/status/404")
|
|
144
|
+
except RetryExhausted as e:
|
|
145
|
+
print(f"{e.attempts} 次重试失败: {e.last_exception}")
|
|
146
|
+
except StatusException as e:
|
|
147
|
+
print(f"HTTP {e.code}")
|
|
148
|
+
except RequestException as e:
|
|
149
|
+
print(f"请求异常: {e}")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 文档
|
|
153
|
+
|
|
154
|
+
完整文档见 `docs/` 目录,本地预览:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
cd docs
|
|
158
|
+
pnpm install
|
|
159
|
+
pnpm run dev
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Rspress website
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
Install the dependencies:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Get started
|
|
12
|
+
|
|
13
|
+
Start the dev server:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Build the website for production:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm run build
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Preview the production build locally:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm run preview
|
|
29
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"text": "指南",
|
|
4
|
+
"link": "/guide/introduction",
|
|
5
|
+
"activeMatch": "/guide/"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"text": "使用教程",
|
|
9
|
+
"link": "/usage/basic-request",
|
|
10
|
+
"activeMatch": "/usage/"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"text": "API 参考",
|
|
14
|
+
"link": "/api/net",
|
|
15
|
+
"activeMatch": "/api/"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
["net", "sync-net", "response", "config", "exceptions"]
|