mockpath 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.
- mockpath-0.1.0/.gitignore +10 -0
- mockpath-0.1.0/.python-version +1 -0
- mockpath-0.1.0/PKG-INFO +161 -0
- mockpath-0.1.0/README.md +144 -0
- mockpath-0.1.0/api/v1/users/list.get.resp.1.json +1 -0
- mockpath-0.1.0/api/v1/users/list.get.resp.json +1 -0
- mockpath-0.1.0/api/v1/users/list.get.yaml +9 -0
- mockpath-0.1.0/api/v1/users/profile.post.resp.1.json +1 -0
- mockpath-0.1.0/api/v1/users/profile.post.resp.json +1 -0
- mockpath-0.1.0/api/v1/users/profile.post.yaml +5 -0
- mockpath-0.1.0/pyproject.toml +29 -0
- mockpath-0.1.0/src/mockpath/__init__.py +3 -0
- mockpath-0.1.0/src/mockpath/cli.py +208 -0
- mockpath-0.1.0/uv.lock +103 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.10
|
mockpath-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mockpath
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight HTTP mock server — directory structure as URL paths
|
|
5
|
+
Project-URL: Homepage, https://github.com/seanpm2001/mockpath
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: api,http,mock,server,testing
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
12
|
+
Classifier: Topic :: Software Development :: Testing
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: click>=8.0
|
|
15
|
+
Requires-Dist: pyyaml
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# mockpath
|
|
19
|
+
|
|
20
|
+
轻量级 HTTP Mock 服务器。通过 YAML 配置 + JSON 文件定义模拟接口,目录结构即 URL 路径。
|
|
21
|
+
|
|
22
|
+
## 特性
|
|
23
|
+
|
|
24
|
+
- 目录结构自动映射为 URL 路径
|
|
25
|
+
- YAML 定义接口配置,JSON 定义请求/响应体
|
|
26
|
+
- 支持 query 参数匹配(子集匹配,请求中的额外参数不影响匹配)
|
|
27
|
+
- 支持请求体匹配(深度相等比较)
|
|
28
|
+
- 支持内联响应、文件引用响应、约定命名响应三种方式
|
|
29
|
+
- `--reload` 模式自动监听文件变更并热重载
|
|
30
|
+
- 未知路径返回 404,方法不匹配返回 405
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install mockpath
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 使用
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
mockpath [-p PORT] [-d DIR] [--reload]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| 参数 | 说明 | 默认值 |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `-p, --port` | 监听端口 | 8000 |
|
|
47
|
+
| `-d, --dir` | 配置目录 | `./api` |
|
|
48
|
+
| `--reload` | 监听文件变更,自动重载配置 | 关闭 |
|
|
49
|
+
|
|
50
|
+
## 文件命名规则
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
api/ # 配置根目录(通过 -d 指定)
|
|
54
|
+
v1/
|
|
55
|
+
users/
|
|
56
|
+
list.get.yaml # GET /v1/users/list 的配置
|
|
57
|
+
list.get.resp.json # 默认响应
|
|
58
|
+
list.get.resp.1.json # 匹配规则 #1 的响应
|
|
59
|
+
profile.post.yaml # POST /v1/users/profile 的配置
|
|
60
|
+
profile.post.resp.json # 默认响应
|
|
61
|
+
profile.post.req.1.json # 匹配规则 #1 的请求体
|
|
62
|
+
profile.post.resp.1.json # 匹配规则 #1 的响应
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
命名模式:`<端点名>.<HTTP方法>.yaml`
|
|
66
|
+
|
|
67
|
+
## YAML 配置格式
|
|
68
|
+
|
|
69
|
+
### 基础接口(无匹配规则)
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
# profile.get.yaml
|
|
73
|
+
status: 200
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
响应体来自 `profile.get.resp.json`。
|
|
77
|
+
|
|
78
|
+
### Query 参数匹配
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
# list.get.yaml
|
|
82
|
+
status: 200
|
|
83
|
+
matches:
|
|
84
|
+
- params:
|
|
85
|
+
page: "1"
|
|
86
|
+
limit: "10"
|
|
87
|
+
- params:
|
|
88
|
+
page: "2"
|
|
89
|
+
response: # 内联响应
|
|
90
|
+
users: []
|
|
91
|
+
total: 0
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
匹配逻辑:请求的 query 参数是配置参数的超集即可匹配(子集匹配)。
|
|
95
|
+
|
|
96
|
+
### 请求体匹配(POST/PUT/PATCH)
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
# profile.post.yaml
|
|
100
|
+
status: 201
|
|
101
|
+
matches:
|
|
102
|
+
- request: # 内联请求体
|
|
103
|
+
name: "Bob"
|
|
104
|
+
response_file: resp.1.json
|
|
105
|
+
- request_file: req.2.json # 引用外部请求体文件
|
|
106
|
+
response:
|
|
107
|
+
id: 4
|
|
108
|
+
name: "Alice"
|
|
109
|
+
- status: 200 # 约定命名:请求体来自 profile.post.req.3.json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 请求体的三种指定方式
|
|
113
|
+
|
|
114
|
+
每条匹配规则的请求体按以下优先级确定:
|
|
115
|
+
|
|
116
|
+
1. **`request`** — YAML 中的内联 JSON 请求体
|
|
117
|
+
2. **`request_file`** — 引用外部文件(相对于 YAML 文件的路径)
|
|
118
|
+
3. **约定命名** — 自动查找 `<端点名>.<方法>.req.N.json`
|
|
119
|
+
|
|
120
|
+
### 响应的三种指定方式
|
|
121
|
+
|
|
122
|
+
每条匹配规则的响应按以下优先级确定:
|
|
123
|
+
|
|
124
|
+
1. **`response`** — YAML 中的内联 JSON 响应
|
|
125
|
+
2. **`response_file`** — 引用外部文件(相对于 YAML 文件的路径)
|
|
126
|
+
3. **约定命名** — 自动查找 `<端点名>.<方法>.resp.N.json`
|
|
127
|
+
|
|
128
|
+
## 匹配流程
|
|
129
|
+
|
|
130
|
+
1. 按 `matches` 列表顺序逐一尝试
|
|
131
|
+
2. **第一个匹配成功的规则生效**(first match wins)
|
|
132
|
+
3. 无匹配 → 返回默认响应
|
|
133
|
+
|
|
134
|
+
## 示例
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# 启动服务器
|
|
138
|
+
mockpath -p 3000
|
|
139
|
+
|
|
140
|
+
# Query 参数匹配
|
|
141
|
+
curl "http://localhost:3000/v1/users/list?page=1"
|
|
142
|
+
# → [{"id": 1, "name": "Alice"}]
|
|
143
|
+
|
|
144
|
+
curl "http://localhost:3000/v1/users/list?page=2"
|
|
145
|
+
# → {"users": [], "total": 0}
|
|
146
|
+
|
|
147
|
+
# 请求体匹配
|
|
148
|
+
curl -X POST http://localhost:3000/v1/users/profile \
|
|
149
|
+
-H "Content-Type: application/json" \
|
|
150
|
+
-d '{"name": "Bob"}'
|
|
151
|
+
# → {"id": 3, "name": "Bob"}
|
|
152
|
+
|
|
153
|
+
# 无匹配时返回默认响应
|
|
154
|
+
curl "http://localhost:3000/v1/users/list"
|
|
155
|
+
# → [{"id": 1, "name": "Alice"}]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 依赖
|
|
159
|
+
|
|
160
|
+
- Python >= 3.10
|
|
161
|
+
- [PyYAML](https://pyyaml.org/)
|
mockpath-0.1.0/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# mockpath
|
|
2
|
+
|
|
3
|
+
轻量级 HTTP Mock 服务器。通过 YAML 配置 + JSON 文件定义模拟接口,目录结构即 URL 路径。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 目录结构自动映射为 URL 路径
|
|
8
|
+
- YAML 定义接口配置,JSON 定义请求/响应体
|
|
9
|
+
- 支持 query 参数匹配(子集匹配,请求中的额外参数不影响匹配)
|
|
10
|
+
- 支持请求体匹配(深度相等比较)
|
|
11
|
+
- 支持内联响应、文件引用响应、约定命名响应三种方式
|
|
12
|
+
- `--reload` 模式自动监听文件变更并热重载
|
|
13
|
+
- 未知路径返回 404,方法不匹配返回 405
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install mockpath
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 使用
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
mockpath [-p PORT] [-d DIR] [--reload]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
| 参数 | 说明 | 默认值 |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `-p, --port` | 监听端口 | 8000 |
|
|
30
|
+
| `-d, --dir` | 配置目录 | `./api` |
|
|
31
|
+
| `--reload` | 监听文件变更,自动重载配置 | 关闭 |
|
|
32
|
+
|
|
33
|
+
## 文件命名规则
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
api/ # 配置根目录(通过 -d 指定)
|
|
37
|
+
v1/
|
|
38
|
+
users/
|
|
39
|
+
list.get.yaml # GET /v1/users/list 的配置
|
|
40
|
+
list.get.resp.json # 默认响应
|
|
41
|
+
list.get.resp.1.json # 匹配规则 #1 的响应
|
|
42
|
+
profile.post.yaml # POST /v1/users/profile 的配置
|
|
43
|
+
profile.post.resp.json # 默认响应
|
|
44
|
+
profile.post.req.1.json # 匹配规则 #1 的请求体
|
|
45
|
+
profile.post.resp.1.json # 匹配规则 #1 的响应
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
命名模式:`<端点名>.<HTTP方法>.yaml`
|
|
49
|
+
|
|
50
|
+
## YAML 配置格式
|
|
51
|
+
|
|
52
|
+
### 基础接口(无匹配规则)
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
# profile.get.yaml
|
|
56
|
+
status: 200
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
响应体来自 `profile.get.resp.json`。
|
|
60
|
+
|
|
61
|
+
### Query 参数匹配
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
# list.get.yaml
|
|
65
|
+
status: 200
|
|
66
|
+
matches:
|
|
67
|
+
- params:
|
|
68
|
+
page: "1"
|
|
69
|
+
limit: "10"
|
|
70
|
+
- params:
|
|
71
|
+
page: "2"
|
|
72
|
+
response: # 内联响应
|
|
73
|
+
users: []
|
|
74
|
+
total: 0
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
匹配逻辑:请求的 query 参数是配置参数的超集即可匹配(子集匹配)。
|
|
78
|
+
|
|
79
|
+
### 请求体匹配(POST/PUT/PATCH)
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
# profile.post.yaml
|
|
83
|
+
status: 201
|
|
84
|
+
matches:
|
|
85
|
+
- request: # 内联请求体
|
|
86
|
+
name: "Bob"
|
|
87
|
+
response_file: resp.1.json
|
|
88
|
+
- request_file: req.2.json # 引用外部请求体文件
|
|
89
|
+
response:
|
|
90
|
+
id: 4
|
|
91
|
+
name: "Alice"
|
|
92
|
+
- status: 200 # 约定命名:请求体来自 profile.post.req.3.json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 请求体的三种指定方式
|
|
96
|
+
|
|
97
|
+
每条匹配规则的请求体按以下优先级确定:
|
|
98
|
+
|
|
99
|
+
1. **`request`** — YAML 中的内联 JSON 请求体
|
|
100
|
+
2. **`request_file`** — 引用外部文件(相对于 YAML 文件的路径)
|
|
101
|
+
3. **约定命名** — 自动查找 `<端点名>.<方法>.req.N.json`
|
|
102
|
+
|
|
103
|
+
### 响应的三种指定方式
|
|
104
|
+
|
|
105
|
+
每条匹配规则的响应按以下优先级确定:
|
|
106
|
+
|
|
107
|
+
1. **`response`** — YAML 中的内联 JSON 响应
|
|
108
|
+
2. **`response_file`** — 引用外部文件(相对于 YAML 文件的路径)
|
|
109
|
+
3. **约定命名** — 自动查找 `<端点名>.<方法>.resp.N.json`
|
|
110
|
+
|
|
111
|
+
## 匹配流程
|
|
112
|
+
|
|
113
|
+
1. 按 `matches` 列表顺序逐一尝试
|
|
114
|
+
2. **第一个匹配成功的规则生效**(first match wins)
|
|
115
|
+
3. 无匹配 → 返回默认响应
|
|
116
|
+
|
|
117
|
+
## 示例
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# 启动服务器
|
|
121
|
+
mockpath -p 3000
|
|
122
|
+
|
|
123
|
+
# Query 参数匹配
|
|
124
|
+
curl "http://localhost:3000/v1/users/list?page=1"
|
|
125
|
+
# → [{"id": 1, "name": "Alice"}]
|
|
126
|
+
|
|
127
|
+
curl "http://localhost:3000/v1/users/list?page=2"
|
|
128
|
+
# → {"users": [], "total": 0}
|
|
129
|
+
|
|
130
|
+
# 请求体匹配
|
|
131
|
+
curl -X POST http://localhost:3000/v1/users/profile \
|
|
132
|
+
-H "Content-Type: application/json" \
|
|
133
|
+
-d '{"name": "Bob"}'
|
|
134
|
+
# → {"id": 3, "name": "Bob"}
|
|
135
|
+
|
|
136
|
+
# 无匹配时返回默认响应
|
|
137
|
+
curl "http://localhost:3000/v1/users/list"
|
|
138
|
+
# → [{"id": 1, "name": "Alice"}]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 依赖
|
|
142
|
+
|
|
143
|
+
- Python >= 3.10
|
|
144
|
+
- [PyYAML](https://pyyaml.org/)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"id": 1, "name": "Alice"}]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{"id": 1, "name": "Alice"}]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id": 3, "name": "Bob"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"ok": true}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mockpath"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Lightweight HTTP mock server — directory structure as URL paths"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = ["pyyaml", "click>=8.0"]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
keywords = ["mock", "http", "server", "testing", "api"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Topic :: Software Development :: Testing",
|
|
15
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
mockpath = "mockpath.cli:main"
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/seanpm2001/mockpath"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/mockpath"]
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import urlparse, parse_qs
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class MatchEntry:
|
|
15
|
+
params: dict[str, str] | None
|
|
16
|
+
request_body: object | None
|
|
17
|
+
status: int
|
|
18
|
+
response: object
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class RouteEntry:
|
|
23
|
+
status: int
|
|
24
|
+
default_response: object
|
|
25
|
+
matches: list[MatchEntry] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_specs(spec_dir: Path) -> dict[tuple[str, str], RouteEntry]:
|
|
29
|
+
routes: dict[tuple[str, str], RouteEntry] = {}
|
|
30
|
+
|
|
31
|
+
for yaml_path in sorted(spec_dir.rglob("*.yaml")):
|
|
32
|
+
parts = yaml_path.stem.split(".")
|
|
33
|
+
if len(parts) < 2:
|
|
34
|
+
continue
|
|
35
|
+
name, method = parts[0], parts[1].upper()
|
|
36
|
+
|
|
37
|
+
rel = yaml_path.relative_to(spec_dir).parent
|
|
38
|
+
url_path = "/" + "/".join([*rel.parts, name]) if rel.parts else "/" + name
|
|
39
|
+
|
|
40
|
+
with open(yaml_path) as f:
|
|
41
|
+
config = yaml.safe_load(f) or {}
|
|
42
|
+
|
|
43
|
+
default_status = config.get("status", 200)
|
|
44
|
+
|
|
45
|
+
resp_file = yaml_path.with_name(f"{name}.{parts[1]}.resp.json")
|
|
46
|
+
default_response = None
|
|
47
|
+
if resp_file.exists():
|
|
48
|
+
with open(resp_file) as f:
|
|
49
|
+
default_response = json.load(f)
|
|
50
|
+
|
|
51
|
+
matches: list[MatchEntry] = []
|
|
52
|
+
for i, m in enumerate(config.get("matches", []), start=1):
|
|
53
|
+
match_status = m.get("status", default_status)
|
|
54
|
+
|
|
55
|
+
# Resolve response: inline > response_file > convention
|
|
56
|
+
if "response" in m:
|
|
57
|
+
match_response = m["response"]
|
|
58
|
+
elif "response_file" in m:
|
|
59
|
+
rf = yaml_path.parent / m["response_file"]
|
|
60
|
+
with open(rf) as f:
|
|
61
|
+
match_response = json.load(f)
|
|
62
|
+
else:
|
|
63
|
+
conv_file = yaml_path.with_name(f"{name}.{parts[1]}.resp.{i}.json")
|
|
64
|
+
match_response = None
|
|
65
|
+
if conv_file.exists():
|
|
66
|
+
with open(conv_file) as f:
|
|
67
|
+
match_response = json.load(f)
|
|
68
|
+
|
|
69
|
+
# Resolve request body: inline > request_file > convention
|
|
70
|
+
req_body = None
|
|
71
|
+
if "request" in m:
|
|
72
|
+
req_body = m["request"]
|
|
73
|
+
elif "request_file" in m:
|
|
74
|
+
rf = yaml_path.parent / m["request_file"]
|
|
75
|
+
with open(rf) as f:
|
|
76
|
+
req_body = json.load(f)
|
|
77
|
+
else:
|
|
78
|
+
req_file = yaml_path.with_name(f"{name}.{parts[1]}.req.{i}.json")
|
|
79
|
+
if req_file.exists():
|
|
80
|
+
with open(req_file) as f:
|
|
81
|
+
req_body = json.load(f)
|
|
82
|
+
|
|
83
|
+
matches.append(MatchEntry(
|
|
84
|
+
params=m.get("params"),
|
|
85
|
+
request_body=req_body,
|
|
86
|
+
status=match_status,
|
|
87
|
+
response=match_response,
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
routes[(method, url_path)] = RouteEntry(
|
|
91
|
+
status=default_status,
|
|
92
|
+
default_response=default_response,
|
|
93
|
+
matches=matches,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return routes
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
routes: dict[tuple[str, str], RouteEntry] = {}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MockHandler(BaseHTTPRequestHandler):
|
|
103
|
+
def handle_request(self):
|
|
104
|
+
parsed = urlparse(self.path)
|
|
105
|
+
path = parsed.path.rstrip("/") or "/"
|
|
106
|
+
method = self.command.upper()
|
|
107
|
+
query = parse_qs(parsed.query)
|
|
108
|
+
query_flat = {k: v[0] if len(v) == 1 else v for k, v in query.items()}
|
|
109
|
+
|
|
110
|
+
path_exists = any(p == path for (_, p) in routes)
|
|
111
|
+
|
|
112
|
+
route = routes.get((method, path))
|
|
113
|
+
if not route:
|
|
114
|
+
status = 405 if path_exists else 404
|
|
115
|
+
self.send_response(status)
|
|
116
|
+
self.send_header("Content-Type", "application/json")
|
|
117
|
+
self.end_headers()
|
|
118
|
+
self.wfile.write(json.dumps({"error": "Method Not Allowed" if status == 405 else "Not Found"}).encode())
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
body = None
|
|
122
|
+
body_read = False
|
|
123
|
+
for m in route.matches:
|
|
124
|
+
if m.params is not None:
|
|
125
|
+
if all(query_flat.get(k) == v for k, v in m.params.items()):
|
|
126
|
+
self._send(m.status, m.response)
|
|
127
|
+
return
|
|
128
|
+
elif m.request_body is not None:
|
|
129
|
+
if not body_read:
|
|
130
|
+
body = self._read_body()
|
|
131
|
+
body_read = True
|
|
132
|
+
if body == m.request_body:
|
|
133
|
+
self._send(m.status, m.response)
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
self._send(route.status, route.default_response)
|
|
137
|
+
|
|
138
|
+
def _read_body(self):
|
|
139
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
140
|
+
if length == 0:
|
|
141
|
+
return None
|
|
142
|
+
try:
|
|
143
|
+
return json.loads(self.rfile.read(length))
|
|
144
|
+
except (json.JSONDecodeError, ValueError):
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def _send(self, status: int, body: object):
|
|
148
|
+
self.send_response(status)
|
|
149
|
+
self.send_header("Content-Type", "application/json")
|
|
150
|
+
self.end_headers()
|
|
151
|
+
if body is not None:
|
|
152
|
+
self.wfile.write(json.dumps(body).encode())
|
|
153
|
+
|
|
154
|
+
def log_message(self, format, *args):
|
|
155
|
+
click.echo(f" {self.command} {self.path} → {args[1] if len(args) > 1 else '?'}")
|
|
156
|
+
|
|
157
|
+
do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = handle_request
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def watch_reload(spec_dir: Path):
|
|
161
|
+
def snapshot():
|
|
162
|
+
result = {}
|
|
163
|
+
for p in spec_dir.rglob("*"):
|
|
164
|
+
if p.is_file() and p.suffix in (".yaml", ".json"):
|
|
165
|
+
result[str(p)] = p.stat().st_mtime
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
mtimes = snapshot()
|
|
169
|
+
while True:
|
|
170
|
+
time.sleep(2)
|
|
171
|
+
current = snapshot()
|
|
172
|
+
if current != mtimes:
|
|
173
|
+
mtimes = current
|
|
174
|
+
global routes
|
|
175
|
+
routes = load_specs(spec_dir)
|
|
176
|
+
click.echo(" [reload] Specs reloaded")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@click.command()
|
|
180
|
+
@click.option("-p", "--port", default=8000, type=int, help="Port to listen on.")
|
|
181
|
+
@click.option("-d", "--dir", "spec_dir", default="./api", type=click.Path(exists=True, file_okay=False), help="Spec directory.")
|
|
182
|
+
@click.option("--reload", is_flag=True, help="Watch for file changes and auto-reload.")
|
|
183
|
+
@click.version_option(package_name="mockpath")
|
|
184
|
+
def main(port: int, spec_dir: str, reload: bool):
|
|
185
|
+
"""Lightweight HTTP mock server — directory structure as URL paths."""
|
|
186
|
+
spec_path = Path(spec_dir).resolve()
|
|
187
|
+
|
|
188
|
+
global routes
|
|
189
|
+
routes = load_specs(spec_path)
|
|
190
|
+
|
|
191
|
+
click.echo(f"mockpath listening on http://localhost:{port}")
|
|
192
|
+
click.echo(f" spec dir: {spec_path}")
|
|
193
|
+
click.echo(f" routes loaded: {len(routes)}")
|
|
194
|
+
for (method, path) in sorted(routes):
|
|
195
|
+
r = routes[(method, path)]
|
|
196
|
+
match_info = f" ({len(r.matches)} matches)" if r.matches else ""
|
|
197
|
+
click.echo(f" {method:6s} {path}{match_info}")
|
|
198
|
+
|
|
199
|
+
if reload:
|
|
200
|
+
threading.Thread(target=watch_reload, args=(spec_path,), daemon=True).start()
|
|
201
|
+
click.echo(" watching for changes...")
|
|
202
|
+
|
|
203
|
+
server = HTTPServer(("", port), MockHandler)
|
|
204
|
+
try:
|
|
205
|
+
server.serve_forever()
|
|
206
|
+
except KeyboardInterrupt:
|
|
207
|
+
click.echo("\nShutting down.")
|
|
208
|
+
server.shutdown()
|
mockpath-0.1.0/uv.lock
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.10"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "click"
|
|
7
|
+
version = "8.3.1"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
11
|
+
]
|
|
12
|
+
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
|
13
|
+
wheels = [
|
|
14
|
+
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "colorama"
|
|
19
|
+
version = "0.4.6"
|
|
20
|
+
source = { registry = "https://pypi.org/simple" }
|
|
21
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
22
|
+
wheels = [
|
|
23
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[[package]]
|
|
27
|
+
name = "mockpath"
|
|
28
|
+
version = "0.1.0"
|
|
29
|
+
source = { editable = "." }
|
|
30
|
+
dependencies = [
|
|
31
|
+
{ name = "click" },
|
|
32
|
+
{ name = "pyyaml" },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[package.metadata]
|
|
36
|
+
requires-dist = [
|
|
37
|
+
{ name = "click", specifier = ">=8.0" },
|
|
38
|
+
{ name = "pyyaml" },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[[package]]
|
|
42
|
+
name = "pyyaml"
|
|
43
|
+
version = "6.0.3"
|
|
44
|
+
source = { registry = "https://pypi.org/simple" }
|
|
45
|
+
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
|
46
|
+
wheels = [
|
|
47
|
+
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
|
|
48
|
+
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
|
|
49
|
+
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
|
|
50
|
+
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
|
|
51
|
+
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
|
|
52
|
+
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
|
|
53
|
+
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
|
|
54
|
+
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
|
|
55
|
+
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
|
|
56
|
+
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
|
57
|
+
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
|
58
|
+
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
|
59
|
+
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
|
60
|
+
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
|
61
|
+
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
|
62
|
+
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
|
63
|
+
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
|
64
|
+
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
|
65
|
+
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
|
66
|
+
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
|
67
|
+
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
|
68
|
+
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
|
69
|
+
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
|
70
|
+
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
|
71
|
+
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
|
73
|
+
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
|
74
|
+
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
|
75
|
+
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
|
76
|
+
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
|
77
|
+
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
|
78
|
+
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
|
79
|
+
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
|
80
|
+
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
|
81
|
+
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
|
82
|
+
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
|
83
|
+
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
|
84
|
+
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
|
85
|
+
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
|
86
|
+
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
|
87
|
+
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
|
88
|
+
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
|
89
|
+
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
|
90
|
+
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
|
91
|
+
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
|
92
|
+
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
|
93
|
+
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
|
94
|
+
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
|
95
|
+
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
|
96
|
+
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
|
97
|
+
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
|
98
|
+
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
|
99
|
+
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
|
100
|
+
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
|
101
|
+
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
|
102
|
+
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
|
103
|
+
]
|